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.
Files changed (64) hide show
  1. data/.yardopts +1 -10
  2. data/README.markdown +7 -14
  3. data/ext/accessibility/key_coder/key_coder.c +7 -0
  4. data/lib/AXElements.rb +0 -2
  5. data/lib/accessibility/core.rb +180 -123
  6. data/lib/accessibility/dsl.rb +310 -191
  7. data/lib/accessibility/enumerators.rb +9 -8
  8. data/lib/accessibility/errors.rb +7 -8
  9. data/lib/accessibility/factory.rb +16 -9
  10. data/lib/accessibility/graph.rb +68 -22
  11. data/lib/accessibility/highlighter.rb +86 -0
  12. data/lib/accessibility/pp_inspector.rb +4 -4
  13. data/lib/accessibility/qualifier.rb +11 -9
  14. data/lib/accessibility/string.rb +12 -4
  15. data/lib/accessibility/translator.rb +19 -10
  16. data/lib/accessibility/version.rb +3 -1
  17. data/lib/accessibility.rb +42 -17
  18. data/lib/ax/application.rb +90 -30
  19. data/lib/ax/button.rb +5 -2
  20. data/lib/ax/element.rb +133 -149
  21. data/lib/ax/pop_up_button.rb +12 -0
  22. data/lib/ax/radio_button.rb +5 -2
  23. data/lib/ax/row.rb +2 -2
  24. data/lib/ax/static_text.rb +5 -2
  25. data/lib/ax/systemwide.rb +24 -12
  26. data/lib/ax_elements/awesome_print.rb +13 -0
  27. data/lib/ax_elements/exception_workaround.rb +5 -0
  28. data/lib/ax_elements/nsarray_compat.rb +1 -0
  29. data/lib/ax_elements.rb +2 -1
  30. data/lib/minitest/ax_elements.rb +60 -4
  31. data/lib/mouse.rb +47 -20
  32. data/lib/rspec/expectations/ax_elements.rb +180 -88
  33. data/rakelib/doc.rake +7 -0
  34. data/test/helper.rb +2 -1
  35. data/test/integration/accessibility/test_dsl.rb +126 -18
  36. data/test/integration/accessibility/test_errors.rb +1 -1
  37. data/test/integration/ax/test_element.rb +17 -0
  38. data/test/integration/minitest/test_ax_elements.rb +33 -38
  39. data/test/integration/rspec/expectations/test_ax_elements.rb +68 -19
  40. data/test/sanity/accessibility/test_core.rb +45 -37
  41. data/test/sanity/accessibility/test_highlighter.rb +56 -0
  42. data/test/sanity/ax/test_application.rb +8 -0
  43. data/test/sanity/ax/test_element.rb +7 -3
  44. data/test/sanity/minitest/test_ax_elements.rb +2 -0
  45. data/test/sanity/rspec/expectations/test_ax_elements.rb +3 -0
  46. data/test/sanity/test_accessibility.rb +9 -0
  47. data/test/sanity/test_mouse.rb +2 -2
  48. metadata +11 -38
  49. data/docs/AccessibilityTips.markdown +0 -119
  50. data/docs/Acting.markdown +0 -340
  51. data/docs/Debugging.markdown +0 -165
  52. data/docs/Inspecting.markdown +0 -261
  53. data/docs/KeyboardEvents.markdown +0 -122
  54. data/docs/NewBehaviour.markdown +0 -151
  55. data/docs/Notifications.markdown +0 -271
  56. data/docs/Searching.markdown +0 -250
  57. data/docs/TestingExtensions.markdown +0 -52
  58. data/docs/images/all_the_buttons.jpg +0 -0
  59. data/docs/images/next_version.png +0 -0
  60. data/docs/images/ui_hierarchy.dot +0 -34
  61. data/docs/images/ui_hierarchy.png +0 -0
  62. data/lib/accessibility/debug.rb +0 -164
  63. data/test/integration/accessibility/test_debug.rb +0 -44
  64. data/test/sanity/accessibility/test_debug.rb +0 -63
@@ -5,18 +5,16 @@ require 'ax/element'
5
5
  require 'ax/application'
6
6
  require 'ax/systemwide'
7
7
  require 'accessibility'
8
- require 'accessibility/debug'
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
- # {file:docs/Acting.markdown Acting tutorial} for examples on how to use
19
- # methods from this module.
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. This does not guarantee it will be
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
- # If the application is not already running, it will be
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
- # app_with_identifier 'com.apple.finder'
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
- # @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.
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 avoid cases where developers assumed an element would have
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: CFRangeMake(1,10)
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
- # {file:docs/KeyboardEvents.markdown Keyboard Events}.
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 to
355
- # performing an explicit search on an element except that the search filters
356
- # take two extra options which can control how long to wait and from where
357
- # to start searches from. You __MUST__ supply either the parent or ancestor
358
- # options to specify where to search from. Searching from the parent implies
359
- # that what you are waiting for is a child of the parent and not a more
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] opts
379
- # @options opts [Number] :timeout (15)
380
- # @options opts [AX::Element] :parent
381
- # @options opts [AX::Element] :ancestor
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, opts = {}, &block
384
- if opts.has_key? :ancestor
385
- wait_for_descendant element, opts.delete(:ancestor), opts, &block
386
- elsif opts.has_key? :parent
387
- wait_for_child element, opts.delete(:parent), opts, &block
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 opt required'
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, opts, &block
403
- timeout = opts.delete(:timeout) || 15
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, opts, &block)
353
+ result = ancestor.search(descendant, filters, &block)
407
354
  return result unless result.blank?
408
- sleep 0.2
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 option must be the parent of the element you are
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, opts, &block
430
- timeout = opts.delete(:timeout) || 15
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, opts, &block)
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.2
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 Interaction
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 CGPointMake(100, 100)
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::Debug.on? && arg.respond_to?(:bounds)
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 hightlight an area of the screen.
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 an argument is provided then the mouse will move to that point
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
- Mouse.click
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
- def highlight element, opts = {}
553
- Accessibility::Debug.highlight element, opts
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
- def path_for element
557
- Accessibility::Debug.path element
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
- def subtree_for element
561
- # @todo Create Element#descendants
562
- Accessibility::Debug.text_subtree element
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
- # @note This is an unfinished feature
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
- # Make a `dot` format graph of the tree, meant for graphing with
569
- # GraphViz.
647
+ # @example
570
648
  #
571
- # @return [String]
572
- def graph element, open = true
573
- Accessibility::Debug.graph_subtree element
574
- # @todo Use the `open` flag to decide if it should be sent to
575
- # graphviz and opened right away
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 Misc.
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
- # Get the current mouse position and return the top most element at
594
- # that point.
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
- # @param [#to_point]
607
- # @return [AX::Element]
608
- def element_at_point point, for: app
609
- app.element_at point
610
- end
611
-
612
- ##
613
- # Get the top most object at an arbitrary point on the screen. The
614
- # given point can be a CGPoint, an Array, or anything else that
615
- # responds to `#to_point`.
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
- def element_at_point point
619
- system_wide.element_at point
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 table until the given element is visible.
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 [Boolean]
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 popup menu to an item in the menu and then move the
669
- # mouse pointer to that item.
777
+ # Scroll a menu to an item in the menu and then move the mouse
778
+ # pointer to that item.
670
779
  #
671
- # @param [AX::Element]
672
- # @return [Boolean]
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
- direction = element.position.y > menu.position.y ? -5 : 5
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
- Mouse.scroll direction
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.kind_of? AX::Menu
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