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.
- data/.yardopts +0 -4
- data/README.markdown +22 -17
- data/Rakefile +1 -1
- data/ext/accessibility/key_coder/extconf.rb +1 -1
- data/ext/accessibility/key_coder/key_coder.c +2 -4
- data/lib/accessibility.rb +3 -3
- data/lib/accessibility/core.rb +948 -0
- data/lib/accessibility/dsl.rb +30 -186
- data/lib/accessibility/enumerators.rb +1 -0
- data/lib/accessibility/factory.rb +78 -134
- data/lib/accessibility/graph.rb +5 -9
- data/lib/accessibility/highlighter.rb +86 -0
- data/lib/accessibility/{pretty_printer.rb → pp_inspector.rb} +4 -3
- data/lib/accessibility/qualifier.rb +3 -5
- data/lib/accessibility/screen_recorder.rb +217 -0
- data/lib/accessibility/statistics.rb +57 -0
- data/lib/accessibility/translator.rb +23 -32
- data/lib/accessibility/version.rb +2 -22
- data/lib/ax/application.rb +20 -159
- data/lib/ax/element.rb +42 -32
- data/lib/ax/scroll_area.rb +5 -6
- data/lib/ax/systemwide.rb +1 -33
- data/lib/ax_elements.rb +1 -9
- data/lib/ax_elements/core_graphics_workaround.rb +5 -0
- data/lib/ax_elements/nsarray_compat.rb +17 -97
- data/lib/ax_elements/vendor/inflection_data.rb +66 -0
- data/lib/ax_elements/vendor/inflections.rb +176 -0
- data/lib/ax_elements/vendor/inflector.rb +306 -0
- data/lib/minitest/ax_elements.rb +180 -0
- data/lib/mouse.rb +227 -0
- data/lib/rspec/expectations/ax_elements.rb +234 -0
- data/rakelib/gem.rake +3 -12
- data/rakelib/test.rake +15 -0
- data/test/helper.rb +20 -10
- data/test/integration/accessibility/test_core.rb +18 -0
- data/test/integration/accessibility/test_dsl.rb +40 -38
- data/test/integration/accessibility/test_enumerators.rb +1 -0
- data/test/integration/accessibility/test_graph.rb +0 -1
- data/test/integration/accessibility/test_qualifier.rb +2 -2
- data/test/integration/ax/test_application.rb +2 -9
- data/test/integration/ax/test_element.rb +0 -40
- data/test/integration/minitest/test_ax_elements.rb +89 -0
- data/test/integration/rspec/expectations/test_ax_elements.rb +102 -0
- data/test/sanity/accessibility/test_factory.rb +2 -2
- data/test/sanity/accessibility/test_highlighter.rb +56 -0
- data/test/sanity/accessibility/{test_pretty_printer.rb → test_pp_inspector.rb} +9 -9
- data/test/sanity/accessibility/test_statistics.rb +57 -0
- data/test/sanity/ax/test_application.rb +1 -16
- data/test/sanity/ax/test_element.rb +2 -2
- data/test/sanity/ax_elements/test_nsobject_inspect.rb +2 -4
- data/test/sanity/minitest/test_ax_elements.rb +17 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +15 -0
- data/test/sanity/test_mouse.rb +22 -0
- data/test/test_core.rb +454 -0
- metadata +44 -69
- data/History.markdown +0 -41
- data/lib/accessibility/system_info.rb +0 -230
- data/lib/ax_elements/active_support_selections.rb +0 -10
- data/lib/ax_elements/mri.rb +0 -57
- 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
|