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
@@ -1,5 +1,6 @@
1
1
  require 'ax/element'
2
2
  require 'accessibility/qualifier'
3
+ require 'accessibility/dsl'
3
4
 
4
5
  ##
5
6
  # AXElements assertions for MiniTest.
@@ -53,6 +54,34 @@ class MiniTest::Assertions
53
54
  end
54
55
  alias_method :assert_has_descendant, :assert_has_descendent
55
56
 
57
+ ##
58
+ # Test that an element will have a child/descendent soon. This method
59
+ # will block until the element is found or a timeout occurs.
60
+ #
61
+ # This is a minitest front end to using {DSL#wait_for}, so any
62
+ # parameters you would normally pass to that method will work here.
63
+ # This also means that you must include either a `parent` key or an
64
+ # `ancestor` key as one of the filters.
65
+ #
66
+ # @param [#to_s]
67
+ # @param [Hash]
68
+ # @yield An optional block to be used in the search qualifier
69
+ def assert_shortly_has kind, filters = {}, &block
70
+ # need to know if parent/ancestor now because wait_for eats some keys
71
+ (ancest = filters[:ancestor]) || (parent = filters[:parent])
72
+ msg = message {
73
+ descend = ax_search_id kind, filters, block
74
+ if ancest
75
+ "Expected #{ancest.inspect} to have descendent #{descend} before a timeout occurred"
76
+ else
77
+ "Expected #{parent.inspect} to have child #{descend} before a timeout occurred"
78
+ end
79
+ }
80
+ result = wait_for kind, filters, &block
81
+ refute result.blank?, msg
82
+ result
83
+ end
84
+
56
85
  ##
57
86
  # Test that an element _does not_ have a specific child. For example,
58
87
  # test that a row is no longer in a table. You can pass any filters
@@ -69,7 +98,7 @@ class MiniTest::Assertions
69
98
  def refute_has_child parent, kind, filters = {}, &block
70
99
  result = ax_check_children parent, kind, filters, block
71
100
  msg = message {
72
- "Expected #{parent.inspect} not to have #{result} as a child"
101
+ "Expected #{parent.inspect} NOT to have #{result} as a child"
73
102
  }
74
103
  assert result.blank?, msg
75
104
  result
@@ -91,13 +120,42 @@ class MiniTest::Assertions
91
120
  def refute_has_descendent ancestor, kind, filters = {}, &block
92
121
  result = ax_check_descendent ancestor, kind, filters, block
93
122
  msg = message {
94
- "Expected #{ancestor.inspect} not to have #{result} as a descendent"
123
+ "Expected #{ancestor.inspect} NOT to have #{result} as a descendent"
95
124
  }
96
125
  assert result.blank?, msg
97
126
  result
98
127
  end
99
128
  alias_method :refute_has_descendant, :refute_has_descendent
100
129
 
130
+ ##
131
+ # @todo Does having this assertion make sense? I've only added it
132
+ # for the time being because OCD demands it.
133
+ #
134
+ # Test that an element will NOT have a child/descendent soon. This
135
+ # method will block until the element is found or a timeout occurs.
136
+ #
137
+ # This is a minitest front end to using {DSL#wait_for}, so any
138
+ # parameters you would normally pass to that method will work here.
139
+ # This also means that you must include either a `parent` key or an
140
+ # `ancestor` key as one of the filters.
141
+ #
142
+ # @param [#to_s]
143
+ # @param [Hash]
144
+ # @yield An optional block to be used in the search qualifier
145
+ def refute_shortly_has kind, filters = {}, &block
146
+ result = wait_for kind, filters, &block
147
+ msg = message {
148
+ if ancest = filters[:ancestor]
149
+ "Expected #{ancest.inspect} NOT to have #{result.inspect} as a descendent"
150
+ else
151
+ parent = filters[:parent]
152
+ "Expected #{parent.inspect} NOT to have #{result.inspect} as a child"
153
+ end
154
+ }
155
+ assert result.blank?, msg
156
+ result
157
+ end
158
+
101
159
 
102
160
  private
103
161
 
@@ -115,5 +173,3 @@ class MiniTest::Assertions
115
173
  end
116
174
 
117
175
  end
118
-
119
- # @todo assertions for minitest/spec
data/lib/mouse.rb CHANGED
@@ -1,14 +1,14 @@
1
1
  framework 'ApplicationServices'
2
2
 
3
3
  ##
4
+ # This is a first attempt at writing a wrapper around the CoreGraphics event
5
+ # taps API provided by OS X. The module provides a simple Ruby interface to
6
+ # performing mouse interactions such as moving and clicking.
7
+ #
4
8
  # [Reference](http://developer.apple.com/library/mac/#documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html).
5
9
  #
6
- # @todo Inertial scrolling
7
- # @todo Bezier paths
8
- # @todo More intelligent default duration
9
- # @todo Refactor to try and reuse the same event for a single action
10
- # instead of creating new events.
11
- # @todo Pause between down/up clicks
10
+ # A rewrite is in the works, but in the mean time this code base still works
11
+ # despite its warts.
12
12
  module Mouse
13
13
  extend self
14
14
 
@@ -20,7 +20,7 @@ module Mouse
20
20
 
21
21
  ##
22
22
  # @note We keep the number as a rational to try and avoid rounding
23
- # error introduced by the way MacRuby deals with floats.
23
+ # error introduced by the floats, especially MacRuby floats.
24
24
  #
25
25
  # Smallest unit of time allowed for an animation step.
26
26
  #
@@ -95,12 +95,36 @@ module Mouse
95
95
  ##
96
96
  # A standard click. Default position is the current position.
97
97
  #
98
+ # This `duration` parameter is used to add a minimum time between
99
+ # down and up click as clicking up too fast has shown to screw things up in
100
+ # some cases. However, the duration is arbitrary and if the system lags it
101
+ # could cause certain timing behaviours (consider the two behaviours of
102
+ # pop up buttons).
103
+ #
98
104
  # @param [CGPoint]
99
105
  def click point = current_position, duration = 12
106
+ click_down
107
+ sleep QUANTUM*duration
108
+ click_up
109
+ end
110
+
111
+ ##
112
+ # Perform a down click. You should follow this up with a call to
113
+ # {#click_up} to finish the click.
114
+ #
115
+ # @param [CGPoint]
116
+ def click_down point = current_position
100
117
  event = new_event KCGEventLeftMouseDown, point, KCGMouseButtonLeft
101
118
  post event
102
- duration.times { sleep QUANTUM }
103
- set event, to: KCGEventLeftMouseUp
119
+ end
120
+
121
+ ##
122
+ # Perform an up click. This should only be called after a call to
123
+ # {#click_down} to finish the click event.
124
+ #
125
+ # @param [CGPoint]
126
+ def click_up point = current_position
127
+ event = new_event KCGEventLeftMouseUp, point, KCGMouseButtonLeft
104
128
  post event
105
129
  end
106
130
 
@@ -111,8 +135,8 @@ module Mouse
111
135
  def secondary_click point = current_position, duration = 12
112
136
  event = new_event KCGEventRightMouseDown, point, KCGMouseButtonRight
113
137
  post event
114
- duration.times { sleep QUANTUM }
115
- set event, to: KCGEventRightMouseUp
138
+ sleep QUANTUM*duration
139
+ set_type event, KCGEventRightMouseUp
116
140
  post event
117
141
  end
118
142
  alias_method :right_click, :secondary_click
@@ -124,13 +148,13 @@ module Mouse
124
148
  def double_click point = current_position
125
149
  event = new_event KCGEventLeftMouseDown, point, KCGMouseButtonLeft
126
150
  post event
127
- set event, to: KCGEventLeftMouseUp
151
+ set_type event, KCGEventLeftMouseUp
128
152
  post event
129
153
 
130
154
  CGEventSetIntegerValueField(event, KCGMouseEventClickState, 2)
131
- set event, to: KCGEventLeftMouseDown
155
+ set_type event, KCGEventLeftMouseDown
132
156
  post event
133
- set event, to: KCGEventLeftMouseUp
157
+ set_type event, KCGEventLeftMouseUp
134
158
  post event
135
159
  end
136
160
 
@@ -149,11 +173,11 @@ module Mouse
149
173
  # @param [CGPoint]
150
174
  # @param [Number]
151
175
  def arbitrary_click point = current_position, button = KCGMouseButtonCenter, duration = 12
152
- event = CGEventCreateMouseEvent(nil, KCGEventOtherMouseDown, point, button)
153
- CGEventPost(KCGHIDEventTap, event)
154
- duration.times do sleep QUANTUM end
155
- CGEventSetType(event, KCGEventOtherMouseUp)
156
- CGEventPost(KCGHIDEventTap, event)
176
+ event = new_event KCGEventOtherMouseDown, point, button
177
+ post event
178
+ sleep QUANTUM*duration
179
+ set_type event, KCGEventOtherMouseUp
180
+ post event
157
181
  end
158
182
  alias_method :other_click, :arbitrary_click
159
183
 
@@ -204,7 +228,10 @@ module Mouse
204
228
  CGEventPost(KCGHIDEventTap, event)
205
229
  end
206
230
 
207
- def set event, to: state
231
+ ##
232
+ # Change the event type for an instance of an event. This is how you would
233
+ # reuse a specific event. In most cases, reusing events is a necessity.
234
+ def set_type event, state
208
235
  CGEventSetType(event, state)
209
236
  end
210
237
 
@@ -1,56 +1,161 @@
1
+ require 'accessibility/dsl'
1
2
  require 'accessibility/qualifier'
2
3
  require 'ax/element'
3
4
 
4
- ##
5
- # Custom matcher for RSpec to check if an element has the specified
6
- # child element.
7
- class Accessibility::HasChildMatcher
8
-
9
- # @param [#to_s]
10
- # @param [Hash]
11
- # @yield
12
- def initialize kind, filters, &block
13
- @qualifier = Accessibility::Qualifier.new(kind, filters, &block)
14
- end
5
+ module Accessibility
15
6
 
16
- # @param [AX::Element]
17
- def matches? parent
18
- !search(parent).blank?
19
- end
7
+ ##
8
+ # @abstract
9
+ #
10
+ # Base class for RSpec matchers used with AXElements.
11
+ class AbstractMatcher
20
12
 
21
- # @return [String]
22
- def failure_message_for_should
23
- "Expected #@parent to have child #{@qualifier.describe}"
24
- end
13
+ # @return [#to_s]
14
+ attr_reader :kind
25
15
 
26
- # @param [AX::Element]
27
- def does_not_match? parent
28
- search(parent).blank?
29
- end
16
+ # @return [Hash{Symbol=>Object}]
17
+ attr_reader :filters
18
+
19
+ # @return [Proc]
20
+ attr_reader :block
21
+
22
+ # @param [#to_s]
23
+ # @param [Hash]
24
+ # @yield
25
+ def initialize kind, filters, &block
26
+ @kind, @filters, @block = kind, filters, block
27
+ end
30
28
 
31
- # @return [String]
32
- def failure_message_for_should_not
33
- "Expected #@parent to NOT have child #@result"
29
+ # @param [AX::Element]
30
+ def does_not_match? element
31
+ !matches?(element)
32
+ end
33
+
34
+
35
+ private
36
+
37
+ # @return [Accessibility::Qualifier]
38
+ def qualifier
39
+ @qualifier ||= Accessibility::Qualifier.new(kind, filters, &block)
40
+ end
34
41
  end
35
42
 
36
43
  ##
37
- # Implemented to override `NSObject#description`.
38
- #
39
- # @return [String]
40
- def description
41
- "should have a child that matches #{@qualifier.describe}"
44
+ # Custom matcher for RSpec to check if an element has the specified
45
+ # child element.
46
+ class HasChildMatcher < AbstractMatcher
47
+ # @param [AX::Element]
48
+ def matches? parent
49
+ @parent = parent
50
+ @result = parent.children.find { |x| qualifier.qualifies? x }
51
+ !@result.blank?
52
+ end
53
+
54
+ # @return [String]
55
+ def failure_message_for_should
56
+ "Expected #@parent to have child #{qualifier.describe}"
57
+ end
58
+
59
+ # @return [String]
60
+ def failure_message_for_should_not
61
+ "Expected #@parent to NOT have child #@result"
62
+ end
63
+
64
+ # @return [String]
65
+ def description
66
+ "should have a child that matches #{qualifier.describe}"
67
+ end
42
68
  end
43
69
 
70
+ ##
71
+ # Custom matcher for RSpec to check if an element has the specified
72
+ # descendent element.
73
+ class HasDescendentMatcher < AbstractMatcher
74
+ # @param [AX::Element]
75
+ def matches? ancestor
76
+ @ancestor = ancestor
77
+ @result = ancestor.search(kind, filters, &block)
78
+ !@result.blank?
79
+ end
80
+
81
+ # @return [String]
82
+ def failure_message_for_should
83
+ "Expected #@ancestor to have descendent #{qualifier.describe}"
84
+ end
85
+
86
+ # @return [String]
87
+ def failure_message_for_should_not
88
+ "Expected #@ancestor to NOT have descendent #@result"
89
+ end
90
+
91
+ # @return [String]
92
+ def description
93
+ "should have a descendent matching #{qualifier.describe}"
94
+ end
95
+ end
44
96
 
45
- private
46
-
47
- def search parent
48
- @parent = parent
49
- @result = parent.children.find { |x| @qualifier.qualifies? x }
97
+ ##
98
+ # Custom matcher for RSpec to check if an element has the specified
99
+ # child element within a grace period. Used for testing things
100
+ # after an asynchronous action is performed.
101
+ class HasChildShortlyMatcher < AbstractMatcher
102
+ include DSL
103
+
104
+ # @param [AX::Element]
105
+ def matches? parent
106
+ @filters[:parent] = @parent = parent
107
+ @result = wait_for kind, filters, &block
108
+ !@result.blank?
109
+ end
110
+
111
+ # @return [String]
112
+ def failure_message_for_should
113
+ "Expected #@parent to have child #{qualifier.describe} before a timeout occurred"
114
+ end
115
+
116
+ # @return [String]
117
+ def failure_message_for_should_not
118
+ "Expected #@parent to NOT have child #@result before a timeout occurred"
119
+ end
120
+
121
+ # @return [String]
122
+ def description
123
+ "should have a child that matches #{qualifier.describe} before a timeout occurs"
124
+ end
50
125
  end
51
126
 
127
+ ##
128
+ # Custom matcher for RSpec to check if an element has the specified
129
+ # descendent element within a grace period. Used for testing things
130
+ # after an asynchronous action is performed.
131
+ class HasDescendentShortlyMatcher < AbstractMatcher
132
+ include DSL
133
+
134
+ # @param [AX::Element]
135
+ def matches? ancestor
136
+ @filters[:ancestor] = @ancestor = ancestor
137
+ @result = wait_for kind, filters, &block
138
+ !@result.blank?
139
+ end
140
+
141
+ # @return [String]
142
+ def failure_message_for_should
143
+ "Expected #@ancestor to have descendent #{qualifier.describe} before a timeout occurred"
144
+ end
145
+
146
+ # @return [String]
147
+ def failure_message_for_should_not
148
+ "Expected #@ancestor to NOT have descendent #@result before a timeout occurred"
149
+ end
150
+
151
+ # @return [String]
152
+ def description
153
+ "should have a descendent matching #{qualifier.describe} before a timeout occurs"
154
+ end
155
+ end
52
156
  end
53
157
 
158
+
54
159
  ##
55
160
  # Assert that the receiving element has the specified child element. You
56
161
  # can use any filters you would normally use in a search, including
@@ -70,58 +175,6 @@ def have_child kind, filters = {}, &block
70
175
  Accessibility::HasChildMatcher.new kind, filters, &block
71
176
  end
72
177
 
73
-
74
- ##
75
- # Custom matcher for RSpec to check if an element has the specified
76
- # descendent element.
77
- class Accessibility::HasDescendentMatcher
78
-
79
- # @param [#to_s]
80
- # @param [Hash]
81
- # @yield
82
- def initialize kind, filters, &block
83
- @kind, @filters, @block = kind, filters, block
84
- @qualifier = Accessibility::Qualifier.new(@kind, @filters, &@block)
85
- end
86
-
87
- # @param [AX::Element]
88
- def matches? ancestor
89
- !search(ancestor).blank?
90
- end
91
-
92
- # @return [String]
93
- def failure_message_for_should
94
- "Expected #@ancestor to have descendent #{@qualifier.describe}"
95
- end
96
-
97
- # @param [AX::Element]
98
- def does_not_match? ancestor
99
- search(ancestor).blank?
100
- end
101
-
102
- # @return [String]
103
- def failure_message_for_should_not
104
- "Expected #@ancestor to NOT have descendent #@result"
105
- end
106
-
107
- ##
108
- # Implemented to override `NSObject#description`.
109
- #
110
- # @return [String]
111
- def description
112
- "should have a descendent matching #{@qualifier.describe}"
113
- end
114
-
115
-
116
- private
117
-
118
- def search ancestor
119
- @ancestor = ancestor
120
- @result = ancestor.search(@kind, @filters, &@block)
121
- end
122
-
123
- end
124
-
125
178
  ##
126
179
  # Assert that the given element has the specified descendent. You can
127
180
  # pass any parameters you normally would use during a search,
@@ -140,3 +193,42 @@ def have_descendent kind, filters = {}, &block
140
193
  Accessibility::HasDescendentMatcher.new kind, filters, &block
141
194
  end
142
195
  alias :have_descendant :have_descendent
196
+
197
+ ##
198
+ # Assert that the given element has the specified child soon. This
199
+ # method will block until the child is found or a timeout occurs. You
200
+ # can pass any parameters you normally would use during a search,
201
+ # including a block.
202
+ #
203
+ # @example
204
+ #
205
+ # app.main_window.should shortly_have_child(:row, static_text: { value: 'Cake' })
206
+ #
207
+ # row.should_not shortly_have_child(:check_box)
208
+ #
209
+ # @param [#to_s]
210
+ # @param [Hash]
211
+ # @yield An optional block to be used as part of the search qualifier
212
+ def shortly_have_child kind, filters = {}, &block
213
+ Accessibility::HasChildShortlyMatcher.new(kind, filters, &block)
214
+ end
215
+
216
+ ##
217
+ # Assert that the given element has the specified descendent soon. This
218
+ # method will block until the descendent is found or a timeout occurs.
219
+ # You can pass any parameters you normally would use during a search,
220
+ # including a block.
221
+ #
222
+ # @example
223
+ #
224
+ # app.main_window.should shortly_have_child(:row, static_text: { value: 'Cake' })
225
+ #
226
+ # row.should_not shortly_have_child(:check_box)
227
+ #
228
+ # @param [#to_s]
229
+ # @param [Hash]
230
+ # @yield An optional block to be used as part of the search qualifier
231
+ def shortly_have_descendent kind, filters = {}, &block
232
+ Accessibility::HasDescendentShortlyMatcher.new kind, filters, &block
233
+ end
234
+ alias :shortly_have_descendant :shortly_have_descendent
data/rakelib/doc.rake CHANGED
@@ -11,3 +11,10 @@
11
11
  # task :garden => :yard do
12
12
  # sh 'yard graph --full --dependencies --dot="-Tpng:quartz" -f docs/images/AX.dot'
13
13
  # end
14
+
15
+ desc 'Remove files generated by YARD'
16
+ task :clobber_yard do
17
+ rm_rf '.yardoc/'
18
+ end
19
+ task :clobber => :clobber_yard
20
+
data/test/helper.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  framework 'Cocoa'
2
2
 
3
3
  # We want to launch the test app and make sure it responds to
4
- # accessibility queries, but that is difficult, so we just sleep
4
+ # accessibility queries, but that is difficult to know at what
5
+ # point it will start to respond, so we just sleep
5
6
  APP_BUNDLE_URL = NSURL.fileURLWithPath File.expand_path './test/fixture/Release/AXElementsTester.app'
6
7
  APP_BUNDLE_IDENTIFIER = 'com.marketcircle.AXElementsTester'
7
8