AXElements 0.7.8 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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