AXElements 0.6.0beta1

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 (54) hide show
  1. data/.yardopts +20 -0
  2. data/LICENSE.txt +25 -0
  3. data/README.markdown +150 -0
  4. data/Rakefile +109 -0
  5. data/docs/AccessibilityTips.markdown +119 -0
  6. data/docs/Acting.markdown +340 -0
  7. data/docs/Debugging.markdown +326 -0
  8. data/docs/Inspecting.markdown +255 -0
  9. data/docs/KeyboardEvents.markdown +57 -0
  10. data/docs/NewBehaviour.markdown +151 -0
  11. data/docs/Notifications.markdown +271 -0
  12. data/docs/Searching.markdown +250 -0
  13. data/docs/TestingExtensions.markdown +52 -0
  14. data/docs/images/AX.png +0 -0
  15. data/docs/images/all_the_buttons.jpg +0 -0
  16. data/docs/images/ui_hierarchy.dot +34 -0
  17. data/docs/images/ui_hierarchy.png +0 -0
  18. data/ext/key_coder/extconf.rb +6 -0
  19. data/ext/key_coder/key_coder.m +77 -0
  20. data/lib/ax_elements/accessibility/enumerators.rb +104 -0
  21. data/lib/ax_elements/accessibility/language.rb +347 -0
  22. data/lib/ax_elements/accessibility/qualifier.rb +73 -0
  23. data/lib/ax_elements/accessibility.rb +164 -0
  24. data/lib/ax_elements/core.rb +541 -0
  25. data/lib/ax_elements/element.rb +593 -0
  26. data/lib/ax_elements/elements/application.rb +88 -0
  27. data/lib/ax_elements/elements/button.rb +18 -0
  28. data/lib/ax_elements/elements/radio_button.rb +18 -0
  29. data/lib/ax_elements/elements/row.rb +30 -0
  30. data/lib/ax_elements/elements/static_text.rb +17 -0
  31. data/lib/ax_elements/elements/systemwide.rb +46 -0
  32. data/lib/ax_elements/inspector.rb +116 -0
  33. data/lib/ax_elements/macruby_extensions.rb +255 -0
  34. data/lib/ax_elements/notification.rb +37 -0
  35. data/lib/ax_elements/version.rb +9 -0
  36. data/lib/ax_elements.rb +30 -0
  37. data/lib/minitest/ax_elements.rb +19 -0
  38. data/lib/mouse.rb +185 -0
  39. data/lib/rspec/expectations/ax_elements.rb +15 -0
  40. data/test/elements/test_application.rb +72 -0
  41. data/test/elements/test_row.rb +27 -0
  42. data/test/elements/test_systemwide.rb +38 -0
  43. data/test/helper.rb +119 -0
  44. data/test/test_accessibility.rb +127 -0
  45. data/test/test_blankness.rb +26 -0
  46. data/test/test_core.rb +448 -0
  47. data/test/test_element.rb +939 -0
  48. data/test/test_enumerators.rb +81 -0
  49. data/test/test_inspector.rb +121 -0
  50. data/test/test_language.rb +157 -0
  51. data/test/test_macruby_extensions.rb +303 -0
  52. data/test/test_mouse.rb +5 -0
  53. data/test/test_search_semantics.rb +143 -0
  54. metadata +219 -0
@@ -0,0 +1,77 @@
1
+ /*
2
+ * key_coder.m
3
+ * KeyCoder
4
+ *
5
+ * Created by Mark Rada on 11-07-27.
6
+ * Copyright 2011 Marketcircle Incorporated. All rights reserved.
7
+ */
8
+
9
+ #import <Foundation/Foundation.h>
10
+ #import <Carbon/Carbon.h>
11
+ #import <CoreServices/CoreServices.h>
12
+ #include "ruby/ruby.h"
13
+
14
+ /*
15
+ * @note Static keycode reference at
16
+ * /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
17
+ *
18
+ * Map of characters to key codes.
19
+ *
20
+ * @return [Hash{String=>Fixnum}]
21
+ */
22
+ static NSMutableDictionary* mAX_keycode_map;
23
+
24
+ static VALUE mAX;
25
+
26
+ /*
27
+ * Helper method to create the keycode mapping at runtime.
28
+ */
29
+ static void mAX_initialize_keycode_map() {
30
+
31
+ TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
32
+ CFDataRef keyboardLayoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard,
33
+ kTISPropertyUnicodeKeyLayoutData);
34
+ const UCKeyboardLayout* keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(keyboardLayoutData);
35
+ UInt32 deadKeyState = 0;
36
+ UniCharCount actualStringLength = 0;
37
+ UniChar string[255];
38
+
39
+ mAX_keycode_map = [[NSMutableDictionary alloc] initWithCapacity:255];
40
+
41
+ for (int keyCode = 0; keyCode < 255; keyCode++) {
42
+ UCKeyTranslate (
43
+ keyboardLayout,
44
+ keyCode,
45
+ kUCKeyActionDown,
46
+ 0,
47
+ 0, // kb type
48
+ 0, // OptionBits keyTranslateOptions,
49
+ &deadKeyState,
50
+ 255,
51
+ &actualStringLength,
52
+ string
53
+ );
54
+
55
+ [mAX_keycode_map setObject:[NSNumber numberWithInt:keyCode]
56
+ forKey:[NSString stringWithFormat:@"%C", string[0]]];
57
+ }
58
+
59
+ }
60
+
61
+ void Init_key_coder() {
62
+
63
+ // TODO: Make mapping keys lazy, expose a C function to map a single
64
+ // character to a keycode, and define a hash in Ruby land that
65
+ // will use the hash callback feature to get the mapping on demand.
66
+ // POSSIBLE PROBLEM: How to handle alternative characters, like
67
+ // symbols which require holding shift first? How would we know
68
+ // about them?
69
+
70
+ // Initialize the mapping and expose it as a constant in the AX module
71
+ mAX_initialize_keycode_map();
72
+ mAX = rb_define_module("AX");
73
+ rb_define_const(mAX, "KEYCODE_MAP", (VALUE)mAX_keycode_map);
74
+ // No need to expose the method right now...
75
+ //rb_define_module_function(mAX, "initialize_keycode_map", mAX_initialize_keycode_map, 0);
76
+
77
+ }
@@ -0,0 +1,104 @@
1
+ ##
2
+ # @abstract
3
+ #
4
+ # Common code for all enumerators.
5
+ class Accessibility::AbstractEnumerator
6
+ include Enumerable
7
+
8
+ ##
9
+ # Caches the root.
10
+ #
11
+ # @param [AX::Element] root
12
+ def initialize root
13
+ @root = root
14
+ end
15
+
16
+ end
17
+
18
+ ##
19
+ # Enumerator for visiting each element in a UI hierarchy in breadth
20
+ # first order.
21
+ class Accessibility::BFEnumerator < Accessibility::AbstractEnumerator
22
+
23
+ ##
24
+ # @todo Lazy-wrap element refs, might make things a bit faster
25
+ # for fat trees; what is impact on thin trees?
26
+ # @todo See if we can implement method in a single loop
27
+ #
28
+ # Semi-lazily iterate through the tree.
29
+ #
30
+ # @yieldparam [AX::Element] element a descendant of the root element
31
+ def each
32
+ queue = [@root]
33
+ until queue.empty?
34
+ queue.shift.attribute(:children).each do |x|
35
+ queue << x if x.attributes.include? KAXChildrenAttribute
36
+ yield x
37
+ end
38
+ end
39
+ end
40
+
41
+ ##
42
+ # Explicitly defined so that escaping at the first found element
43
+ # actually works. Since only a single `break` is called when an item
44
+ # is found it does not fully escape the method.
45
+ #
46
+ # Technically, we need to do this with other 'escape-early' iteraters,
47
+ # but they aren't being used...yet.
48
+ def find
49
+ each { |x| return x if yield x }
50
+ end
51
+
52
+ end
53
+
54
+ ##
55
+ # Enumerator for visitng each element in a UI hierarchy in
56
+ # depth first order.
57
+ class Accessibility::DFEnumerator < Accessibility::AbstractEnumerator
58
+
59
+ # @yieldparam [AX::Element] element a descendant of the root
60
+ def each
61
+ stack = @root.attribute(:children)
62
+ until stack.empty?
63
+ current = stack.shift
64
+ yield current
65
+ if current.attributes.include? KAXChildrenAttribute
66
+ # need to reverse it since child ordering seems to matter in practice
67
+ stack.unshift *current.attribute(:children)
68
+ end
69
+ end
70
+ end
71
+
72
+ ##
73
+ # @todo A bit of a hack that I would like to fix one day...
74
+ #
75
+ # Walk the UI element tree and yield both the element and the depth
76
+ # in three relative to the root.
77
+ #
78
+ # @yieldparam [AX::Element]
79
+ # @yieldparam [Number]
80
+ def each_with_height &block
81
+ @root.attribute(:children).each do |element|
82
+ recursive_each_with_height element, 1, block
83
+ end
84
+ end
85
+
86
+
87
+ private
88
+
89
+ ##
90
+ # Recursive implementation of a depth first iterator.
91
+ #
92
+ # @param [AX::Element]
93
+ # @param [Number]
94
+ # @param [#call]
95
+ def recursive_each_with_height element, depth, block
96
+ block.call element, depth
97
+ if element.attributes.include? KAXChildrenAttribute
98
+ element.attribute(:children).each do |x|
99
+ recursive_each_with_height x, depth + 1, block
100
+ end
101
+ end
102
+ end
103
+
104
+ end
@@ -0,0 +1,347 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'mouse'
4
+
5
+ ##
6
+ # @todo Allow the animation duration to be overridden for Mouse stuff?
7
+ #
8
+ # The idea here is to pull actions out from an object and put them
9
+ # in front of object to give AXElements more of a DSL feel to make
10
+ # communicating test steps more clear. See the
11
+ # {file:docs/Acting.markdown Acting tutorial} for examples on how to use
12
+ # methods from this module.
13
+ module Accessibility::Language
14
+
15
+ # @group Actions
16
+
17
+ ##
18
+ # We assume that any method that has the first argument with a type
19
+ # of {AX::Element} is intended to be an action and so `#method_missing`
20
+ # will forward the message to the element.
21
+ #
22
+ # @param [String] method an action constant
23
+ def method_missing method, *args
24
+ arg = args.first
25
+ unless arg.kind_of? AX::Element
26
+ # should be able to just call super, but there is a bug in MacRuby (#1320)
27
+ # so we just recreate what should be happening
28
+ message = "undefined method `#{method}' for #{self}:#{self.class}"
29
+ raise NoMethodError, message
30
+ end
31
+ arg.perform_action method
32
+ end
33
+
34
+ ##
35
+ # Try to perform the `press` action on the given element.
36
+ #
37
+ # @param [AX::Element]
38
+ # @return [Boolean]
39
+ def press element
40
+ element.perform_action :press
41
+ end
42
+
43
+ ##
44
+ # Try to perform the `show_menu` action on the given element.
45
+ #
46
+ # @param [AX::Element]
47
+ # @return [Boolean]
48
+ def show_menu element
49
+ element.perform_action :show_menu
50
+ end
51
+
52
+ ##
53
+ # Try to perform the `pick` action on the given element.
54
+ #
55
+ # @param [AX::Element]
56
+ # @return [Boolean]
57
+ def pick element
58
+ element.perform_action :pick
59
+ end
60
+
61
+ ##
62
+ # Try to perform the `decrement` action on the given element.
63
+ #
64
+ # @param [AX::Element]
65
+ # @return [Boolean]
66
+ def decrement element
67
+ element.perform_action :decrement
68
+ end
69
+
70
+ ##
71
+ # Try to perform the `confirm` action on the given element.
72
+ #
73
+ # @param [AX::Element]
74
+ # @return [Boolean]
75
+ def confirm element
76
+ element.perform_action :confirm
77
+ end
78
+
79
+ ##
80
+ # Try to perform the `increment` action on the given element.
81
+ #
82
+ # @param [AX::Element]
83
+ # @return [Boolean]
84
+ def increment element
85
+ element.perform_action :increment
86
+ end
87
+
88
+ ##
89
+ # Try to perform the `delete` action on the given element.
90
+ #
91
+ # @param [AX::Element]
92
+ # @return [Boolean]
93
+ def delete element
94
+ element.perform_action :delete
95
+ end
96
+
97
+ ##
98
+ # Try to perform the `cancel` action on the given element.
99
+ #
100
+ # @param [AX::Element]
101
+ # @return [Boolean]
102
+ def cancel element
103
+ element.perform_action :cancel
104
+ end
105
+
106
+ ##
107
+ # Tell an app to hide itself.
108
+ #
109
+ # @param [AX::Application]
110
+ # @return [Boolean]
111
+ def hide app
112
+ app.perform_action :hide
113
+ end
114
+
115
+ ##
116
+ # Tell an app to unhide itself, which does not guarantee it will be
117
+ # focused.
118
+ #
119
+ # @param [AX::Application]
120
+ # @return [Boolean]
121
+ def unhide app
122
+ app.perform_action :unhide
123
+ end
124
+
125
+ ##
126
+ # Tell an app to quit.
127
+ #
128
+ # @param [AX::Application]
129
+ # @return [Boolean]
130
+ def terminate app
131
+ app.perform_action :terminate
132
+ end
133
+
134
+ ##
135
+ # @note This method overrides `Kernel#raise` so we have to check the
136
+ # class of the first argument to decide which code path to take.
137
+ #
138
+ # Try to perform the `press` action on the given element.
139
+ #
140
+ # @overload raise element
141
+ # @param [AX::Element] element
142
+ # @return [Boolean]
143
+ #
144
+ # @overload raise exception[, message[, backtrace]]
145
+ # The normal way to raise an exception.
146
+ def raise *args
147
+ arg = args.first
148
+ arg.kind_of?(AX::Element) ? arg.perform_action(:raise) : super
149
+ end
150
+
151
+ ##
152
+ # Focus an element on the screen, but do not set focus again if
153
+ # already focused.
154
+ #
155
+ # @param [AX::Element]
156
+ def set_focus element
157
+ element.set_attribute(:focused, true) unless element.attribute(:focused?)
158
+ end
159
+
160
+ ##
161
+ # @note We try to set focus to the element first; this is to avoid false
162
+ # positives where developers assumed an element would have to have
163
+ # focus before a user could change the value.
164
+ #
165
+ # You would think that the `#set` method should belong to {AX::Element},
166
+ # but I think taking it out of the class and putting it in front helps
167
+ # make the difference between performing actions and inspecting UI more
168
+ # concrete.
169
+ #
170
+ # @overload set element, attribute_name: new_value
171
+ # Set a specified attribute to a new value
172
+ # @param [AX::Element] element
173
+ # @param [Hash{attribute_name=>new_value}] change
174
+ #
175
+ # @overload set element, new_value
176
+ # Set the `value` attribute to a new value
177
+ # @param [AX::Element] element
178
+ # @param [Object] change
179
+ #
180
+ # @return [nil] do not rely on a return value
181
+ def set element, change
182
+ set_focus element if element.attribute_writable? :focused
183
+ key, value = change.is_a?(Hash) ? change.first : [:value, change]
184
+ element.set_attribute key, value
185
+ end
186
+
187
+ ##
188
+ # Simulate keyboard input by typing out the given string. To learn
189
+ # more about how to encode modifier keys (e.g. Command), see the
190
+ # dedicated documentation page on
191
+ # {file:docs/KeyboardEvents.markdown Keyboard Events}.
192
+ #
193
+ # @overload type string
194
+ # Send input to the currently focused application
195
+ # @param [#to_s]
196
+ #
197
+ # @overload type string, app
198
+ # Send input to a specific application
199
+ # @param [#to_s]
200
+ # @param [AX::Application]
201
+ def type string, app = AX::SYSTEM
202
+ app.type_string string.to_s
203
+ end
204
+
205
+ # @group Notifications
206
+
207
+ ##
208
+ # @todo Change this to `register_for_notification:from:` when the
209
+ # syntax is supported by YARD (v0.8) or someone complains,
210
+ # which ever comes first.
211
+ #
212
+ # @param [AX::Element]
213
+ # @param [String]
214
+ def register_for_notification element, notif, &block
215
+ element.on_notification notif, &block
216
+ end
217
+
218
+ ##
219
+ # Pause script execution until notification that has been registered
220
+ # for is received or the full timeout period has passed.
221
+ #
222
+ # If the script is unpaused because of a timeout, then it is assumed
223
+ # that the notification was never received and all notification
224
+ # registrations will be unregistered to avoid future complications.
225
+ #
226
+ # @param [Float] timeout number of seconds to wait for a notification
227
+ def wait_for_notification timeout = 10.0
228
+ AX.wait_for_notif(timeout).tap { |_| unregister_notifications }
229
+ end
230
+
231
+ ##
232
+ # Undo _all_ notification registries.
233
+ def unregister_notifications
234
+ AX.unregister_notifs
235
+ end
236
+
237
+ # @group Mouse Input
238
+
239
+ ##
240
+ # @overload move_mouse_to(element)
241
+ # Move the mouse to a UI element
242
+ # @param [AX::Element]
243
+ #
244
+ # @overload move_mouse_to(point)
245
+ # Move the mouse to an arbitrary point
246
+ # @param [CGPoint]
247
+ #
248
+ # @overload move_mouse_to([x,y])
249
+ # Move the mouse to an arbitrary point given as an two element array
250
+ # @param [Array(Float,Float)]
251
+ def move_mouse_to arg
252
+ Mouse.move_to arg.to_point
253
+ end
254
+
255
+ ##
256
+ # There are many reasons why you would want to cause a drag event
257
+ # with the mouse. Perhaps you want to drag an object to another
258
+ # place, or maybe you want to hightlight an area of the screen.
259
+ #
260
+ # This method will drag the mouse from its current point on the screen
261
+ # to the point given by calling `#to_point` on the argument.
262
+ #
263
+ # Generally, you will pass a {CGPoint} or some kind of {AX::Element},
264
+ # but you could pass anything that responds to #to_point.
265
+ #
266
+ # @param [#to_point]
267
+ def drag_mouse_to arg
268
+ Mouse.drag_to point.to_point
269
+ end
270
+
271
+ ##
272
+ # @todo Need to expose the units option? Would allow scrolling by pixel.
273
+ #
274
+ # Scrolls an arbitrary number of lines at the mouses current point on
275
+ # the screen. Use a positive number to scroll down, and a negative number
276
+ # to scroll up.
277
+ #
278
+ # If the second argument is provided then the mouse will move to that
279
+ # point first; the argument must respond to `#to_point`.
280
+ #
281
+ # @param [Number]
282
+ # @param [#to_point]
283
+ def scroll lines, obj = nil
284
+ move_mouse_to obj if obj
285
+ Mouse.scroll lines
286
+ end
287
+
288
+ ##
289
+ # Perform a regular click.
290
+ #
291
+ # If an argument is provided then the mouse will move to that point
292
+ # first; the argument must respond to `#to_point`.
293
+ #
294
+ # @param [#to_point]
295
+ def click obj = nil
296
+ move_mouse_to obj if obj
297
+ Mouse.click
298
+ end
299
+
300
+ ##
301
+ # Perform a right (aka secondary) click action.
302
+ #
303
+ # If an argument is provided then the mouse will move to that point
304
+ # first; the argument must respond to `#to_point`.
305
+ #
306
+ # @param [#to_point]
307
+ def right_click obj = nil
308
+ move_mouse_to obj if obj
309
+ Mouse.right_click
310
+ end
311
+ alias_method :secondary_click, :right_click
312
+
313
+ ##
314
+ # Perform a double click action.
315
+ #
316
+ # If an argument is provided then the mouse will move to that point
317
+ # first; the argument must respond to `#to_point`.
318
+ #
319
+ # @param [#to_point]
320
+ def double_click obj = nil
321
+ move_mouse_to obj if obj
322
+ Mouse.double_click
323
+ end
324
+
325
+ # @group Macros
326
+
327
+ ##
328
+ # Show the "About" window for an app.
329
+ #
330
+ # @param [AX::Application]
331
+ def show_about_window_for app
332
+ set_focus app
333
+ press app.menu_bar_item(title:(app.title))
334
+ press app.menu_bar.menu_item(title: "About #{app.title}")
335
+ end
336
+
337
+ ##
338
+ # Try to open the preferences for an app using the menu bar.
339
+ #
340
+ # @param [AX::Application]
341
+ def show_preferences_window_for app
342
+ set_focus app
343
+ press app.menu_bar_item(title:(app.title))
344
+ press app.menu_bar.menu_item(title:'Preferences…')
345
+ end
346
+
347
+ end
@@ -0,0 +1,73 @@
1
+ ##
2
+ # Used in searches to answer whether or not a given element meets the
3
+ # expected criteria.
4
+ class Accessibility::Qualifier
5
+
6
+ ##
7
+ # Initialize a qualifier with the kind of object that you want to
8
+ # qualify and a dictionary of filter criteria.
9
+ #
10
+ # @param [String,Symbol]
11
+ # @param [Hash]
12
+ def initialize klass, criteria
13
+ @sym = klass
14
+ @criteria = criteria
15
+ end
16
+
17
+ ##
18
+ # Whether or not a candidate object matches the criteria given
19
+ # at initialization.
20
+ #
21
+ # @param [AX::Element] element
22
+ def qualifies? element
23
+ return false unless the_right_type? element
24
+ return false unless meets_criteria? element
25
+ return true
26
+ end
27
+
28
+
29
+ private
30
+
31
+ ##
32
+ # Checks if a candidate object is of the correct class, respecting
33
+ # that that the class being searched for may not be defined yet.
34
+ #
35
+ # @param [AX::Element]
36
+ def the_right_type? element
37
+ unless @klass
38
+ if AX.const_defined? @sym
39
+ @klass = AX.const_get @sym
40
+ else
41
+ return false
42
+ end
43
+ end
44
+ return element.kind_of? @klass
45
+ end
46
+
47
+ ##
48
+ # @todo How could we handle filters that use parameterized
49
+ # attributes?
50
+ # @todo Optimize searching by compiling filters into an
51
+ # optimized filter qualifier. `eval` is not an option.
52
+ #
53
+ # Determines if the element meets all the criteria of the filters,
54
+ # spawning sub-searches if necessary.
55
+ #
56
+ # @param [AX::Element] element
57
+ def meets_criteria? element
58
+ @criteria.all? do |filter, value|
59
+ if value.kind_of? Hash
60
+ if element.attributes.include? KAXChildrenAttribute
61
+ !element.search(filter, value).blank?
62
+ else
63
+ false
64
+ end
65
+ elsif element.respond_to? filter
66
+ element.send(filter) == value
67
+ else # this legitimately occurs
68
+ false
69
+ end
70
+ end
71
+ end
72
+
73
+ end