AXElements 0.9.0 → 1.0.0.alpha

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 (60) hide show
  1. data/.yardopts +0 -4
  2. data/README.markdown +22 -17
  3. data/Rakefile +1 -1
  4. data/ext/accessibility/key_coder/extconf.rb +1 -1
  5. data/ext/accessibility/key_coder/key_coder.c +2 -4
  6. data/lib/accessibility.rb +3 -3
  7. data/lib/accessibility/core.rb +948 -0
  8. data/lib/accessibility/dsl.rb +30 -186
  9. data/lib/accessibility/enumerators.rb +1 -0
  10. data/lib/accessibility/factory.rb +78 -134
  11. data/lib/accessibility/graph.rb +5 -9
  12. data/lib/accessibility/highlighter.rb +86 -0
  13. data/lib/accessibility/{pretty_printer.rb → pp_inspector.rb} +4 -3
  14. data/lib/accessibility/qualifier.rb +3 -5
  15. data/lib/accessibility/screen_recorder.rb +217 -0
  16. data/lib/accessibility/statistics.rb +57 -0
  17. data/lib/accessibility/translator.rb +23 -32
  18. data/lib/accessibility/version.rb +2 -22
  19. data/lib/ax/application.rb +20 -159
  20. data/lib/ax/element.rb +42 -32
  21. data/lib/ax/scroll_area.rb +5 -6
  22. data/lib/ax/systemwide.rb +1 -33
  23. data/lib/ax_elements.rb +1 -9
  24. data/lib/ax_elements/core_graphics_workaround.rb +5 -0
  25. data/lib/ax_elements/nsarray_compat.rb +17 -97
  26. data/lib/ax_elements/vendor/inflection_data.rb +66 -0
  27. data/lib/ax_elements/vendor/inflections.rb +176 -0
  28. data/lib/ax_elements/vendor/inflector.rb +306 -0
  29. data/lib/minitest/ax_elements.rb +180 -0
  30. data/lib/mouse.rb +227 -0
  31. data/lib/rspec/expectations/ax_elements.rb +234 -0
  32. data/rakelib/gem.rake +3 -12
  33. data/rakelib/test.rake +15 -0
  34. data/test/helper.rb +20 -10
  35. data/test/integration/accessibility/test_core.rb +18 -0
  36. data/test/integration/accessibility/test_dsl.rb +40 -38
  37. data/test/integration/accessibility/test_enumerators.rb +1 -0
  38. data/test/integration/accessibility/test_graph.rb +0 -1
  39. data/test/integration/accessibility/test_qualifier.rb +2 -2
  40. data/test/integration/ax/test_application.rb +2 -9
  41. data/test/integration/ax/test_element.rb +0 -40
  42. data/test/integration/minitest/test_ax_elements.rb +89 -0
  43. data/test/integration/rspec/expectations/test_ax_elements.rb +102 -0
  44. data/test/sanity/accessibility/test_factory.rb +2 -2
  45. data/test/sanity/accessibility/test_highlighter.rb +56 -0
  46. data/test/sanity/accessibility/{test_pretty_printer.rb → test_pp_inspector.rb} +9 -9
  47. data/test/sanity/accessibility/test_statistics.rb +57 -0
  48. data/test/sanity/ax/test_application.rb +1 -16
  49. data/test/sanity/ax/test_element.rb +2 -2
  50. data/test/sanity/ax_elements/test_nsobject_inspect.rb +2 -4
  51. data/test/sanity/minitest/test_ax_elements.rb +17 -0
  52. data/test/sanity/rspec/expectations/test_ax_elements.rb +15 -0
  53. data/test/sanity/test_mouse.rb +22 -0
  54. data/test/test_core.rb +454 -0
  55. metadata +44 -69
  56. data/History.markdown +0 -41
  57. data/lib/accessibility/system_info.rb +0 -230
  58. data/lib/ax_elements/active_support_selections.rb +0 -10
  59. data/lib/ax_elements/mri.rb +0 -57
  60. data/test/sanity/accessibility/test_version.rb +0 -15
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- require 'active_support/core_ext/object/blank'
4
-
5
3
  require 'mouse'
6
4
  require 'ax/element'
7
5
  require 'ax/application'
@@ -10,8 +8,6 @@ require 'ax/scroll_area'
10
8
  require 'ax/menu'
11
9
  require 'accessibility'
12
10
  require 'accessibility/enumerators'
13
- require 'accessibility/highlighter'
14
-
15
11
 
16
12
  ##
17
13
  # DSL methods for AXElements.
@@ -133,7 +129,7 @@ module Accessibility::DSL
133
129
  # The normal way to raise an exception.
134
130
  def raise *args
135
131
  arg = args.first
136
- arg.kind_of?(AX::Element) ? arg.perform(:raise) : Kernel.raise(*args)
132
+ arg.kind_of?(AX::Element) ? arg.perform(:raise) : super(*args)
137
133
  end
138
134
 
139
135
  ##
@@ -251,7 +247,7 @@ module Accessibility::DSL
251
247
  # @param path [String,Regexp]
252
248
  # @return [Boolean]
253
249
  def select_menu_item app, *path
254
- app.application.select_menu_item *path
250
+ app.select_menu_item *path
255
251
  end
256
252
 
257
253
  ##
@@ -274,7 +270,7 @@ module Accessibility::DSL
274
270
  # @param app [AX::Application]
275
271
  # @return [AX::Window]
276
272
  def show_preferences_window_for app
277
- app.application.show_preferences_window
273
+ app.show_preferences_window
278
274
  end
279
275
 
280
276
  ##
@@ -312,31 +308,6 @@ module Accessibility::DSL
312
308
  element.ancestor(:menu).scroll_to element
313
309
  end
314
310
 
315
- ##
316
- # @note This is a hack to workaround an AXAPI deficiency
317
- # @note This method may move or be renamed in the next release, though
318
- # it will not be removed
319
- #
320
- # Find a contextual menu that is open near the mouses current position
321
- #
322
- # This method assumes that it is being called in the block of a call to
323
- # {#right_click}, and that the contextual menu is going to be very close
324
- # to the mouse pointer.
325
- #
326
- # @return [AX::Menu,nil]
327
- def contextual_menu
328
- # CAST, just like high school trigonometry! :P
329
- c, a, s, t = quads = Array.new(4) { Mouse.current_position }
330
- c.x -= 10; c.y += 10
331
- a.x += 10; a.y += 10
332
- s.x += 10; s.y -= 10
333
- t.x -= 10; t.y -= 10
334
- elements = quads.map { |quad| element_at_point quad }
335
- elements.uniq!
336
- elements.map! { |el| el.kind_of?(AX::Menu) ? el : el.ancestor(:menu) }
337
- elements.find { |element| element.present? }
338
- end
339
-
340
311
 
341
312
  # @!group Polling
342
313
 
@@ -505,7 +476,7 @@ module Accessibility::DSL
505
476
  # @option opts [Number] :wait (0.2) in seconds
506
477
  def move_mouse_to arg, opts = {}
507
478
  duration = opts[:duration] || 0.2
508
- if Accessibility.debug? && arg.kind_of?(AX::Element)
479
+ if Accessibility.debug? && arg.respond_to?(:bounds)
509
480
  highlight arg, timeout: duration, color: NSColor.orangeColor
510
481
  end
511
482
  Mouse.move_to arg.to_point, duration
@@ -555,27 +526,6 @@ module Accessibility::DSL
555
526
  sleep wait
556
527
  end
557
528
 
558
- ##
559
- # @todo Need to expose the units option? Would allow scrolling by pixel.
560
- #
561
- # Horizontally scrolls an arbitrary number of lines at the mouses current
562
- # point on the screen
563
- #
564
- # Use a positive number to scroll left, and a negative number to scroll
565
- # right.
566
- #
567
- # If the second argument is provided then the mouse will move to that
568
- # point first; the argument must respond to `#to_point`.
569
- #
570
- # @param lines [Number]
571
- # @param obj [#to_point]
572
- # @param wait [Number]
573
- def horizontal_scroll lines, obj = nil, wait = 0.1
574
- move_mouse_to obj, wait: 0 if obj
575
- Mouse.horizontal_scroll lines
576
- sleep wait
577
- end
578
-
579
529
  ##
580
530
  # Perform a regular click.
581
531
  #
@@ -600,30 +550,20 @@ module Accessibility::DSL
600
550
  move_mouse_to obj, wait: 0 if obj
601
551
  Mouse.click_down
602
552
  yield if block_given?
603
- ensure
604
553
  Mouse.click_up
605
554
  sleep wait
606
555
  end
607
556
 
608
557
  ##
609
- # Perform a right (a.k.a. secondary) click action.
558
+ # Perform a right (aka secondary) click action.
610
559
  #
611
560
  # If an argument is provided then the mouse will move to that point
612
561
  # first; the argument must respond to `#to_point`.
613
562
  #
614
- # If a block is given, it will be yielded to between the click down
615
- # and click up event. This behaviour is the same as passing a block
616
- # to {DSL#click}.
617
- #
618
- # @yield Optionally take a block that is executed between click down
619
- # and click up events.
620
563
  # @param obj [#to_point]
621
564
  def right_click obj = nil, wait = 0.2
622
565
  move_mouse_to obj, wait: 0 if obj
623
- Mouse.right_click_down
624
- yield if block_given?
625
- ensure
626
- Mouse.right_click_up
566
+ Mouse.right_click
627
567
  sleep wait
628
568
  end
629
569
  alias_method :secondary_click, :right_click
@@ -641,110 +581,6 @@ module Accessibility::DSL
641
581
  sleep wait
642
582
  end
643
583
 
644
- ##
645
- # Perform a triple click action
646
- #
647
- # If an argument is provided then the mouse will move to that point
648
- # first; the argument must respond to `#to_point`.
649
- #
650
- # @param obj [#to_point]
651
- def triple_click obj = nil, wait = 0.2
652
- move_mouse_to obj, wait: 0 if obj
653
- Mouse.triple_click
654
- sleep wait
655
- end
656
-
657
- ##
658
- # Perform a swipe gesture in the given `direction`
659
- #
660
- # Valid directions are:
661
- #
662
- # - `:up`
663
- # - `:down`
664
- # - `:left`
665
- # - `:right`
666
- #
667
- # An optional second argument can be provided. If the argument
668
- # is provided then the mouse pointer will move to that point first.
669
- #
670
- # @example
671
- #
672
- # swipe :left, safari.web_area
673
- #
674
- # @param direction [Symbol]
675
- # @param obj [#to_point]
676
- def swipe direction, obj = nil, wait = 0.2
677
- move_mouse_to obj, wait: 0 if obj
678
- Mouse.swipe direction
679
- sleep wait
680
- end
681
-
682
- ##
683
- # Perform a pinch gesture in the given `direction`
684
- #
685
- # You can optionally specify the `magnification` factor and
686
- # `position` for the pinch event.
687
- #*
688
- # Available pinch directions are:
689
- #
690
- # - `:zoom` or `:expand`
691
- # - `:unzoom` or `:contract`
692
- #
693
- # Magnification is a relative magnification setting. A zoom value of
694
- # `1.0` means `1.0` more than the current zoom level. `2.0` would be
695
- # `2.0` levels higher than the current zoom.
696
- #
697
- # You can also optionally specify an object/point on screen for the mouse
698
- # pointer to be moved to before the gesture begins.
699
- #
700
- # @param direction [Symbol]
701
- # @param magnification [Float]
702
- # @param obj [#to_point]
703
- def pinch direction, magnification = 1, obj = nil, wait = 0.2
704
- move_mouse_to obj, wait: 0 if obj
705
- Mouse.pinch direction, magnification
706
- sleep wait
707
- end
708
-
709
- ##
710
- # Perform a rotation gesture in the given `direction` the given `angle` degrees
711
- #
712
- # Possible directions are:
713
- #
714
- # - `:cw`, ':clockwise`, ':clock_wise` to rotate in the clockwise
715
- # direction
716
- # - `:ccw`, ':counter_clockwise`, `:counter_clock_wise` to rotate in
717
- # the the counter clockwise direction
718
- #
719
- # The `angle` parameter is a number of degrees to rotate. There are 360
720
- # degrees in a full rotation, as you would expect in Euclidian geometry.
721
- #
722
- # You can also optionally specify an object/point on screen for the mouse
723
- # pointer to be moved to before the gesture begins. The movement will
724
- # be instantaneous.
725
- #
726
- # @param direction [Symbol]
727
- # @param angle [Float]
728
- # @param obj [#to_point]
729
- def rotate direction, angle, obj = nil, wait = 0.2
730
- move_mouse_to obj, wait: 0 if obj
731
- Mouse.rotate direction, angle
732
- sleep wait
733
- end
734
-
735
- ##
736
- # Perform a smart magnify (double tap on trackpad)
737
- #
738
- # You can optionally specify an object/point on the screen where to perform
739
- # the smart magnification. The mouse will move to this point first
740
- #
741
- # @param obj [#to_point]
742
- def smart_magnify obj = nil, wait = 0.2
743
- move_mouse_to obj, wait: 0 if obj
744
- Mouse.smart_magnify
745
- sleep wait
746
- end
747
-
748
584
 
749
585
  # @!group Debug Helpers
750
586
 
@@ -775,11 +611,29 @@ module Accessibility::DSL
775
611
  # @option opts [NSColor] :colour (NSColor.magentaColor)
776
612
  # @return [Accessibility::Highlighter]
777
613
  def highlight obj, opts = {}
614
+ require 'accessibility/highlighter'
778
615
  Accessibility::Highlighter.new obj.bounds, opts
779
616
  end
780
617
 
781
618
  ##
782
- # @note This method is currently experimental
619
+ # @deprecated Use {AX::Element#inspect_subtree} instead.
620
+ #
621
+ # Get the dump of the subtree of children and descendants for the given
622
+ # element. Each generation down the tree will be indented another level,
623
+ # and each element will be inspected.
624
+ #
625
+ # @example
626
+ #
627
+ # puts subtree app
628
+ #
629
+ # @param element [AX::Element]
630
+ # @return [String]
631
+ def subtree element
632
+ element.inspect_subtree
633
+ end
634
+ alias_method :subtree_for, :subtree
635
+
636
+ ##
783
637
  # @note You will need to have GraphViz command line tools installed
784
638
  # in order for this to work.
785
639
  #
@@ -843,26 +697,16 @@ module Accessibility::DSL
843
697
  alias_method :capture_screen, :screenshot
844
698
 
845
699
  ##
846
- # See (ScreenRecorder)[http://rdoc.info/gems/screen_recorder/frames]
847
- # for details on the screen recording options
848
- #
849
- # @example
850
- #
851
- # file = record do
852
- # run_tests
853
- # end
854
- # `open '#{file}'`
700
+ # See {Accessibility::ScreenRecorder.record} for details.
855
701
  #
856
702
  # @param file [String]
857
- # @yield
858
- # @yieldparam recorder [ScreenRecorder]
859
703
  # @return [String]
860
704
  def record file = nil, &block
861
- require 'screen_recorder'
705
+ require 'accessibility/screen_recorder'
862
706
  if file
863
- ScreenRecorder.record file, &block
707
+ Accessibility::ScreenRecorder.record file, &block
864
708
  else
865
- ScreenRecorder.record &block
709
+ Accessibility::ScreenRecorder.record &block
866
710
  end
867
711
  end
868
712
 
@@ -881,7 +725,7 @@ module Accessibility::DSL
881
725
  # @param id [String]
882
726
  # @return [AX::Application,nil]
883
727
  def app_with_bundle_identifier id
884
- AX::Application.new id
728
+ Accessibility.application_with_bundle_identifier id
885
729
  end
886
730
  alias_method :app_with_bundle_id, :app_with_bundle_identifier
887
731
  alias_method :launch, :app_with_bundle_identifier
@@ -25,6 +25,7 @@ module Accessibility::Enumerators
25
25
  kids.each do |x| yield x end
26
26
  queue.concat kids
27
27
  end
28
+ []
28
29
  end
29
30
 
30
31
  ##
@@ -1,27 +1,92 @@
1
1
  require 'accessibility/core'
2
2
  require 'accessibility/translator'
3
+ require 'accessibility/statistics'
3
4
 
4
5
  ##
5
6
  # Namespace container for all the accessibility objects.
6
7
  module AX; class Element; end end
7
8
 
8
9
 
9
- class << AX
10
+ ##
11
+ # Extensions to {Accessibility::Element} for the high level abstraction.
12
+ # These extensions only make sense in the context of the high level API
13
+ # and it requires knowledge of both layers, so the code has been placed
14
+ # in its own file.
15
+ module Accessibility::Element
16
+
17
+ ##
18
+ # @todo Should we handle cases where a subrole has a value of
19
+ # 'Unknown'? What is the performance impact?
20
+ #
21
+ # Wrap the low level wrapper with the appropriate high level wrapper.
22
+ # This involves determining the proper class in the {AX} namespace,
23
+ # possibly creating it on demand, and then instantiating the class to
24
+ # wrap the low level object.
25
+ #
26
+ # Some code paths have been unrolled for efficiency. Don't hate player,
27
+ # hate the game.
28
+ #
29
+ # @return [AX::Element]
30
+ def to_ruby
31
+ type = AXValueGetType(self)
32
+ if type.zero?
33
+ to_element
34
+ else
35
+ to_box type
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def to_element
42
+ STATS.increment :Factory
43
+ if roll = self.role
44
+ roll = TRANSLATOR.unprefix roll
45
+ if attributes.include? KAXSubroleAttribute
46
+ subroll = self.subrole
47
+ # Some objects claim to have a subrole but return nil
48
+ if subroll
49
+ class_for2(TRANSLATOR.unprefix(subroll), roll).new self
50
+ else
51
+ class_for(roll).new self
52
+ end
53
+ else
54
+ class_for(roll).new self
55
+ end
56
+ else # failsafe in case object dies before we get the role
57
+ AX::Element.new self
58
+ end
59
+ end
60
+
61
+ def to_box type
62
+ STATS.increment :Unwrap
63
+ ptr = Pointer.new BOX_TYPES[type]
64
+ AXValueGetValue(self, type, ptr)
65
+ ptr.value.to_ruby
66
+ end
67
+
68
+
69
+ private
10
70
 
11
71
  ##
12
72
  # @private
13
73
  #
14
- # Mutex to make sure we only create one class at a time.
74
+ # Reference to the singleton instance of the translator.
15
75
  #
16
- # @return [Mutex]
17
- MUTEX = Mutex.new
76
+ # @return [Accessibility::Translator]
77
+ TRANSLATOR = Accessibility::Translator.instance
18
78
 
19
79
  ##
20
80
  # @private
21
81
  #
22
- # Find the class for a given role
82
+ # Serial queue to make sure we only create one class at a time.
23
83
  #
24
- # If the class does not exist it will be created.
84
+ # @return [Dispatch::Queue]
85
+ CREATE_QUEUE = Dispatch::Queue.new 'com.marketcircle.axelements.create'
86
+
87
+ ##
88
+ # Find the class for a given role. If the class does not exist it will
89
+ # be created.
25
90
  #
26
91
  # @param role [#to_s]
27
92
  # @return [Class]
@@ -34,11 +99,8 @@ class << AX
34
99
  end
35
100
 
36
101
  ##
37
- # @private
38
- #
39
- # Find the class for a given subrole and role
40
- #
41
- # If the class does not exist it will be created on demand.
102
+ # Find the class for a given subrole and role. If the class does not
103
+ # exist it will be created on demand.
42
104
  #
43
105
  # @param subrole [#to_s]
44
106
  # @param role [#to_s]
@@ -52,15 +114,13 @@ class << AX
52
114
  end
53
115
 
54
116
  ##
55
- # @private
56
- #
57
- # Create a class in the {AX} namespace that has {AX::Element} as the
58
- # superclass
117
+ # Create a new class in the {AX} namespace that has {AX::Element}
118
+ # as the superclass.
59
119
  #
60
120
  # @param name [#to_s]
61
121
  # @return [Class]
62
122
  def create_class name
63
- MUTEX.synchronize do
123
+ CREATE_QUEUE.sync do
64
124
  # re-check now that we are in the critical section
65
125
  @klass = if AX.const_defined? name, false
66
126
  AX.const_get name
@@ -73,10 +133,8 @@ class << AX
73
133
  end
74
134
 
75
135
  ##
76
- # @private
77
- #
78
136
  # Create a new class in the {AX} namesapce that has the given
79
- # `superklass` as the superclass
137
+ # `superklass` as the superclass..
80
138
  #
81
139
  # @param name [#to_s]
82
140
  # @param superklass [#to_s]
@@ -85,7 +143,7 @@ class << AX
85
143
  unless AX.const_defined? superklass, false
86
144
  create_class superklass
87
145
  end
88
- MUTEX.synchronize do
146
+ CREATE_QUEUE.sync do
89
147
  # re-check now that we are in the critical section
90
148
  @klass = if AX.const_defined? name, false
91
149
  AX.const_get name
@@ -98,117 +156,3 @@ class << AX
98
156
  end
99
157
 
100
158
  end
101
-
102
-
103
- if on_macruby?
104
-
105
- ##
106
- # Extensions to {Accessibility::Element} for the higher level abstractions
107
- #
108
- # These extensions only make sense in the context of the high level API
109
- # but needs to be applied on the lower level class, so the code has been
110
- # placed in its own file.
111
- module Accessibility::Element
112
-
113
- ##
114
- # @todo Should we handle cases where a subrole has a value of
115
- # 'Unknown'? What is the performance impact?
116
- #
117
- # Wrap the low level wrapper with the appropriate high level wrapper.
118
- # This involves determining the proper class in the {AX} namespace,
119
- # possibly creating it on demand, and then instantiating the class to
120
- # wrap the low level object.
121
- #
122
- # Some code paths have been unrolled for efficiency. Don't hate player,
123
- # hate the game.
124
- #
125
- # @return [AX::Element]
126
- def to_ruby
127
- type = AXValueGetType(self)
128
- if type.zero?
129
- to_element
130
- else
131
- to_box type
132
- end
133
- end
134
-
135
-
136
- private
137
-
138
- ##
139
- # @private
140
- #
141
- # Reference to the singleton instance of the translator.
142
- #
143
- # @return [Accessibility::Translator]
144
- TRANSLATOR = Accessibility::Translator.instance
145
-
146
- def to_box type
147
- ptr = Pointer.new ValueWrapper::BOX_TYPES[type]
148
- AXValueGetValue(self, type, ptr)
149
- ptr.value.to_ruby
150
- end
151
-
152
- def to_element
153
- if roll = self.role
154
- roll = TRANSLATOR.unprefix roll
155
- if attributes.include? KAXSubroleAttribute
156
- subroll = self.subrole
157
- # Some objects claim to have a subrole but return nil
158
- if subroll
159
- AX.class_for2(TRANSLATOR.unprefix(subroll), roll).new self
160
- else
161
- AX.class_for(roll).new self
162
- end
163
- else
164
- AX.class_for(roll).new self
165
- end
166
- else # failsafe in case object dies before we get the role
167
- AX::Element.new self
168
- end
169
- end
170
-
171
- end
172
-
173
-
174
- else
175
-
176
-
177
- ##
178
- # `AXElements` extensions to the `Accessibility::Element` class
179
- class Accessibility::Element
180
-
181
- ##
182
- # Override the default `#to_ruby` so that proper classes are
183
- # chosen for each object.
184
- #
185
- # @return [AX::Element]
186
- def to_ruby
187
- if roll = self.role
188
- roll = TRANSLATOR.unprefix roll
189
- if attributes.include? KAXSubroleAttribute
190
- subroll = self.subrole
191
- # Some objects claim to have a subrole but return nil
192
- if subroll
193
- AX.class_for2(TRANSLATOR.unprefix(subroll), roll).new self
194
- else
195
- AX.class_for(roll).new self
196
- end
197
- else
198
- AX.class_for(roll).new self
199
- end
200
- else # failsafe in case object dies before we get the role
201
- AX::Element.new self
202
- end
203
- end
204
-
205
- ##
206
- # @private
207
- #
208
- # Reference to the singleton instance of the translator.
209
- #
210
- # @return [Accessibility::Translator]
211
- TRANSLATOR = Accessibility::Translator.instance
212
- end
213
-
214
- end