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
@@ -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