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.
- data/.yardopts +20 -0
- data/LICENSE.txt +25 -0
- data/README.markdown +150 -0
- data/Rakefile +109 -0
- data/docs/AccessibilityTips.markdown +119 -0
- data/docs/Acting.markdown +340 -0
- data/docs/Debugging.markdown +326 -0
- data/docs/Inspecting.markdown +255 -0
- data/docs/KeyboardEvents.markdown +57 -0
- data/docs/NewBehaviour.markdown +151 -0
- data/docs/Notifications.markdown +271 -0
- data/docs/Searching.markdown +250 -0
- data/docs/TestingExtensions.markdown +52 -0
- data/docs/images/AX.png +0 -0
- data/docs/images/all_the_buttons.jpg +0 -0
- data/docs/images/ui_hierarchy.dot +34 -0
- data/docs/images/ui_hierarchy.png +0 -0
- data/ext/key_coder/extconf.rb +6 -0
- data/ext/key_coder/key_coder.m +77 -0
- data/lib/ax_elements/accessibility/enumerators.rb +104 -0
- data/lib/ax_elements/accessibility/language.rb +347 -0
- data/lib/ax_elements/accessibility/qualifier.rb +73 -0
- data/lib/ax_elements/accessibility.rb +164 -0
- data/lib/ax_elements/core.rb +541 -0
- data/lib/ax_elements/element.rb +593 -0
- data/lib/ax_elements/elements/application.rb +88 -0
- data/lib/ax_elements/elements/button.rb +18 -0
- data/lib/ax_elements/elements/radio_button.rb +18 -0
- data/lib/ax_elements/elements/row.rb +30 -0
- data/lib/ax_elements/elements/static_text.rb +17 -0
- data/lib/ax_elements/elements/systemwide.rb +46 -0
- data/lib/ax_elements/inspector.rb +116 -0
- data/lib/ax_elements/macruby_extensions.rb +255 -0
- data/lib/ax_elements/notification.rb +37 -0
- data/lib/ax_elements/version.rb +9 -0
- data/lib/ax_elements.rb +30 -0
- data/lib/minitest/ax_elements.rb +19 -0
- data/lib/mouse.rb +185 -0
- data/lib/rspec/expectations/ax_elements.rb +15 -0
- data/test/elements/test_application.rb +72 -0
- data/test/elements/test_row.rb +27 -0
- data/test/elements/test_systemwide.rb +38 -0
- data/test/helper.rb +119 -0
- data/test/test_accessibility.rb +127 -0
- data/test/test_blankness.rb +26 -0
- data/test/test_core.rb +448 -0
- data/test/test_element.rb +939 -0
- data/test/test_enumerators.rb +81 -0
- data/test/test_inspector.rb +121 -0
- data/test/test_language.rb +157 -0
- data/test/test_macruby_extensions.rb +303 -0
- data/test/test_mouse.rb +5 -0
- data/test/test_search_semantics.rb +143 -0
- 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
|