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
data/lib/mouse.rb ADDED
@@ -0,0 +1,227 @@
1
+ framework 'ApplicationServices'
2
+ require 'ax_elements/core_graphics_workaround'
3
+
4
+
5
+ ##
6
+ # This is a first attempt at writing a wrapper around the CoreGraphics event
7
+ # taps API provided by OS X. The module provides a simple Ruby interface to
8
+ # performing mouse interactions such as moving and clicking.
9
+ #
10
+ # [Reference](http://developer.apple.com/library/mac/#documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html).
11
+ #
12
+ # A rewrite is in the works, but in the mean time this code base still works
13
+ # despite its warts.
14
+ module Mouse
15
+ extend self
16
+
17
+ ##
18
+ # Number of animation steps per second.
19
+ #
20
+ # @return [Number]
21
+ FPS = 120
22
+
23
+ ##
24
+ # @note We keep the number as a rational to try and avoid rounding
25
+ # error introduced by the floats, especially MacRuby floats.
26
+ #
27
+ # Smallest unit of time allowed for an animation step.
28
+ #
29
+ # @return [Number]
30
+ QUANTUM = Rational(1, FPS)
31
+
32
+ ##
33
+ # Available constants for the type of units to use when scrolling.
34
+ #
35
+ # @return [Hash{Symbol=>Fixnum}]
36
+ UNIT = {
37
+ line: KCGScrollEventUnitLine,
38
+ pixel: KCGScrollEventUnitPixel
39
+ }
40
+
41
+ ##
42
+ # The coordinates of the mouse using the flipped coordinate system
43
+ # (origin in top left).
44
+ #
45
+ # @return [CGPoint]
46
+ def current_position
47
+ CGEventGetLocation(CGEventCreate(nil))
48
+ end
49
+
50
+ ##
51
+ # Move the mouse from the current position to the given point.
52
+ #
53
+ # @param point [CGPoint]
54
+ # @param duration [Float] animation duration, in seconds
55
+ def move_to point, duration = 0.2
56
+ animate KCGEventMouseMoved, KCGMouseButtonLeft, current_position, point, duration
57
+ end
58
+
59
+ ##
60
+ # Click and drag from the current position to the given point.
61
+ #
62
+ # @param point [CGPoint]
63
+ # @param duration [Float] animation duration, in seconds
64
+ def drag_to point, duration = 0.2
65
+ post new_event(KCGEventLeftMouseDown, current_position, KCGMouseButtonLeft)
66
+
67
+ animate KCGEventLeftMouseDragged, KCGMouseButtonLeft, current_position, point, duration
68
+
69
+ post new_event(KCGEventLeftMouseUp, current_position, KCGMouseButtonLeft)
70
+ end
71
+
72
+ ##
73
+ # @todo Need to double check to see if I introduce any inaccuracies.
74
+ #
75
+ # Scroll at the current position the given amount of units.
76
+ #
77
+ # Scrolling too much or too little in a period of time will cause the
78
+ # animation to look weird, possibly causing the app to mess things up.
79
+ #
80
+ # @param amount [Fixnum] number of units to scroll; positive to scroll
81
+ # up or negative to scroll down
82
+ # @param duration [Float] animation duration, in seconds
83
+ # @param units [Symbol] `:line` scrolls by line, `:pixel` scrolls by pixel
84
+ def scroll amount, duration = 0.2, units = :line
85
+ units = UNIT[units] || raise(ArgumentError, "#{units} is not a valid unit")
86
+ steps = (FPS * duration).round
87
+ current = 0.0
88
+ steps.times do |step|
89
+ done = (step+1).to_f / steps
90
+ scroll = ((done - current)*amount).round
91
+ post new_scroll_event(units, 1, scroll)
92
+ sleep QUANTUM
93
+ current += scroll.to_f / amount
94
+ end
95
+ end
96
+
97
+ ##
98
+ # Perform a down click. You should follow this up with a call to
99
+ # {#click_up} to finish the click.
100
+ #
101
+ # @param point [CGPoint]
102
+ # @param duration [Number]
103
+ def click_down point = current_position, duration = 12
104
+ event = new_event KCGEventLeftMouseDown, point, KCGMouseButtonLeft
105
+ post event
106
+ sleep QUANTUM*duration
107
+ end
108
+
109
+ ##
110
+ # Perform an up click. This should only be called after a call to
111
+ # {#click_down} to finish the click event.
112
+ #
113
+ # @param point [CGPoint]
114
+ def click_up point = current_position
115
+ event = new_event KCGEventLeftMouseUp, point, KCGMouseButtonLeft
116
+ post event
117
+ end
118
+
119
+ ##
120
+ # Standard secondary click. Default position is the current position.
121
+ #
122
+ # @param point [CGPoint]
123
+ # @param duration [Number]
124
+ def secondary_click point = current_position, duration = 12
125
+ event = new_event KCGEventRightMouseDown, point, KCGMouseButtonRight
126
+ post event
127
+ sleep QUANTUM*duration
128
+ set_type event, KCGEventRightMouseUp
129
+ post event
130
+ end
131
+ alias_method :right_click, :secondary_click
132
+
133
+ ##
134
+ # A standard double click. Defaults to clicking at the current position.
135
+ #
136
+ # @param point [CGPoint]
137
+ def double_click point = current_position
138
+ event = new_event KCGEventLeftMouseDown, point, KCGMouseButtonLeft
139
+ post event
140
+ set_type event, KCGEventLeftMouseUp
141
+ post event
142
+
143
+ CGEventSetIntegerValueField(event, KCGMouseEventClickState, 2)
144
+ set_type event, KCGEventLeftMouseDown
145
+ post event
146
+ set_type event, KCGEventLeftMouseUp
147
+ post event
148
+ end
149
+
150
+ ##
151
+ # Click with an arbitrary mouse button, using numbers to represent
152
+ # the mouse button. At the time of writing, the documented values are:
153
+ #
154
+ # - KCGMouseButtonLeft = 0
155
+ # - KCGMouseButtonRight = 1
156
+ # - KCGMouseButtonCenter = 2
157
+ #
158
+ # And the rest are not documented! Though they should be easy enough
159
+ # to figure out. See the `CGMouseButton` enum in the reference
160
+ # documentation for the most up to date list.
161
+ #
162
+ # @param point [CGPoint]
163
+ # @param button [Number]
164
+ # @param duration [Number]
165
+ def arbitrary_click point = current_position, button = KCGMouseButtonCenter, duration = 12
166
+ event = new_event KCGEventOtherMouseDown, point, button
167
+ post event
168
+ sleep QUANTUM*duration
169
+ set_type event, KCGEventOtherMouseUp
170
+ post event
171
+ end
172
+ alias_method :other_click, :arbitrary_click
173
+
174
+
175
+ private
176
+
177
+ ##
178
+ # Executes a mouse movement animation. It can be a simple cursor
179
+ # move or a drag depending on what is passed to `type`.
180
+ def animate type, button, from, to, duration
181
+ current = current_position
182
+ xstep = (to.x - current.x) / (FPS * duration)
183
+ ystep = (to.y - current.y) / (FPS * duration)
184
+ start = NSDate.date
185
+
186
+ until close_enough?(current, to)
187
+ remaining = to.x - current.x
188
+ current.x += xstep.abs > remaining.abs ? remaining : xstep
189
+
190
+ remaining = to.y - current.y
191
+ current.y += ystep.abs > remaining.abs ? remaining : ystep
192
+
193
+ post new_event(type, current, button)
194
+
195
+ sleep QUANTUM
196
+ break if NSDate.date.timeIntervalSinceDate(start) > 5.0
197
+ current = current_position
198
+ end
199
+ end
200
+
201
+ def close_enough? current, target
202
+ x = current.x - target.x
203
+ y = current.y - target.y
204
+ ((x**2)+(y**2)) <= 1
205
+ end
206
+
207
+ def new_event event, position, button
208
+ CGEventCreateMouseEvent(nil, event, position, button)
209
+ end
210
+
211
+ # @param [Fixnum] wheel which scroll wheel to use (value between 1-3)
212
+ def new_scroll_event units, wheel, amount
213
+ CGEventCreateScrollWheelEvent(nil, units, wheel, amount)
214
+ end
215
+
216
+ def post event
217
+ CGEventPost(KCGHIDEventTap, event)
218
+ end
219
+
220
+ ##
221
+ # Change the event type for an instance of an event. This is how you would
222
+ # reuse a specific event. In most cases, reusing events is a necessity.
223
+ def set_type event, state
224
+ CGEventSetType(event, state)
225
+ end
226
+
227
+ end
@@ -0,0 +1,234 @@
1
+ require 'accessibility/dsl'
2
+ require 'accessibility/qualifier'
3
+ require 'ax/element'
4
+
5
+ module Accessibility
6
+
7
+ ##
8
+ # @abstract
9
+ #
10
+ # Base class for RSpec matchers used with AXElements.
11
+ class AbstractMatcher
12
+
13
+ # @return [#to_s]
14
+ attr_reader :kind
15
+
16
+ # @return [Hash{Symbol=>Object}]
17
+ attr_reader :filters
18
+
19
+ # @return [Proc]
20
+ attr_reader :block
21
+
22
+ # @param kind [#to_s]
23
+ # @param filters [Hash]
24
+ # @yield Optional block used for search filtering
25
+ def initialize kind, filters, &block
26
+ @kind, @filters, @block = kind, filters, block
27
+ end
28
+
29
+ # @param element [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
41
+ end
42
+
43
+ ##
44
+ # Custom matcher for RSpec to check if an element has the specified
45
+ # child element.
46
+ class HasChildMatcher < AbstractMatcher
47
+ # @param parent [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
68
+ end
69
+
70
+ ##
71
+ # Custom matcher for RSpec to check if an element has the specified
72
+ # descendent element.
73
+ class HasDescendentMatcher < AbstractMatcher
74
+ # @param ancestor [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
96
+
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 parent [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
125
+ end
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 ancestor [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
156
+ end
157
+
158
+
159
+ ##
160
+ # Assert that the receiving element has the specified child element. You
161
+ # can use any filters you would normally use in a search, including
162
+ # a block.
163
+ #
164
+ # @example
165
+ #
166
+ # window.toolbar.should have_child(:search_field)
167
+ # table.should have_child(:row, static_text: { value: /42/ })
168
+ #
169
+ # search_field.should_not have_child(:busy_indicator)
170
+ #
171
+ # @param kind [#to_s]
172
+ # @param filters [Hash]
173
+ # @yield An optional block to be used as part of the search qualifier
174
+ def have_child kind, filters = {}, &block
175
+ Accessibility::HasChildMatcher.new kind, filters, &block
176
+ end
177
+
178
+ ##
179
+ # Assert that the given element has the specified descendent. You can
180
+ # pass any parameters you normally would use during a search,
181
+ # including a block.
182
+ #
183
+ # @example
184
+ #
185
+ # app.main_window.should have_descendent(:button, title: 'Press Me')
186
+ #
187
+ # row.should_not have_descendent(:check_box)
188
+ #
189
+ # @param kind [#to_s]
190
+ # @param filters [Hash]
191
+ # @yield An optional block to be used as part of the search qualifier
192
+ def have_descendent kind, filters = {}, &block
193
+ Accessibility::HasDescendentMatcher.new kind, filters, &block
194
+ end
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 kind [#to_s]
210
+ # @param filters [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 kind [#to_s]
229
+ # @param filters [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