AXElements 0.7.8 → 0.8.0
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 +1 -10
- data/README.markdown +7 -14
- data/ext/accessibility/key_coder/key_coder.c +7 -0
- data/lib/AXElements.rb +0 -2
- data/lib/accessibility/core.rb +180 -123
- data/lib/accessibility/dsl.rb +310 -191
- data/lib/accessibility/enumerators.rb +9 -8
- data/lib/accessibility/errors.rb +7 -8
- data/lib/accessibility/factory.rb +16 -9
- data/lib/accessibility/graph.rb +68 -22
- data/lib/accessibility/highlighter.rb +86 -0
- data/lib/accessibility/pp_inspector.rb +4 -4
- data/lib/accessibility/qualifier.rb +11 -9
- data/lib/accessibility/string.rb +12 -4
- data/lib/accessibility/translator.rb +19 -10
- data/lib/accessibility/version.rb +3 -1
- data/lib/accessibility.rb +42 -17
- data/lib/ax/application.rb +90 -30
- data/lib/ax/button.rb +5 -2
- data/lib/ax/element.rb +133 -149
- data/lib/ax/pop_up_button.rb +12 -0
- data/lib/ax/radio_button.rb +5 -2
- data/lib/ax/row.rb +2 -2
- data/lib/ax/static_text.rb +5 -2
- data/lib/ax/systemwide.rb +24 -12
- data/lib/ax_elements/awesome_print.rb +13 -0
- data/lib/ax_elements/exception_workaround.rb +5 -0
- data/lib/ax_elements/nsarray_compat.rb +1 -0
- data/lib/ax_elements.rb +2 -1
- data/lib/minitest/ax_elements.rb +60 -4
- data/lib/mouse.rb +47 -20
- data/lib/rspec/expectations/ax_elements.rb +180 -88
- data/rakelib/doc.rake +7 -0
- data/test/helper.rb +2 -1
- data/test/integration/accessibility/test_dsl.rb +126 -18
- data/test/integration/accessibility/test_errors.rb +1 -1
- data/test/integration/ax/test_element.rb +17 -0
- data/test/integration/minitest/test_ax_elements.rb +33 -38
- data/test/integration/rspec/expectations/test_ax_elements.rb +68 -19
- data/test/sanity/accessibility/test_core.rb +45 -37
- data/test/sanity/accessibility/test_highlighter.rb +56 -0
- data/test/sanity/ax/test_application.rb +8 -0
- data/test/sanity/ax/test_element.rb +7 -3
- data/test/sanity/minitest/test_ax_elements.rb +2 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +3 -0
- data/test/sanity/test_accessibility.rb +9 -0
- data/test/sanity/test_mouse.rb +2 -2
- metadata +11 -38
- data/docs/AccessibilityTips.markdown +0 -119
- data/docs/Acting.markdown +0 -340
- data/docs/Debugging.markdown +0 -165
- data/docs/Inspecting.markdown +0 -261
- data/docs/KeyboardEvents.markdown +0 -122
- data/docs/NewBehaviour.markdown +0 -151
- data/docs/Notifications.markdown +0 -271
- data/docs/Searching.markdown +0 -250
- data/docs/TestingExtensions.markdown +0 -52
- data/docs/images/all_the_buttons.jpg +0 -0
- data/docs/images/next_version.png +0 -0
- data/docs/images/ui_hierarchy.dot +0 -34
- data/docs/images/ui_hierarchy.png +0 -0
- data/lib/accessibility/debug.rb +0 -164
- data/test/integration/accessibility/test_debug.rb +0 -44
- data/test/sanity/accessibility/test_debug.rb +0 -63
@@ -88,4 +88,12 @@ class TestAXApplication < MiniTest::Unit::TestCase
|
|
88
88
|
assert_equal running_app.bundleIdentifier, app.bundle_identifier
|
89
89
|
end
|
90
90
|
|
91
|
+
def test_info_plist
|
92
|
+
assert_equal 'hmmmmm.icns', app.info_plist['CFBundleIconFile']
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_version
|
96
|
+
assert_equal '1.0', app.version
|
97
|
+
end
|
98
|
+
|
91
99
|
end
|
@@ -7,6 +7,10 @@ class TestAXElement < MiniTest::Unit::TestCase
|
|
7
7
|
@element ||= AX::Element.new REF
|
8
8
|
end
|
9
9
|
|
10
|
+
def test_ancestry
|
11
|
+
assert_equal [element], element.ancestry
|
12
|
+
end
|
13
|
+
|
10
14
|
def test_children # not much we can do here
|
11
15
|
assert_respond_to element, :children
|
12
16
|
assert_kind_of Array, element.children
|
@@ -38,12 +42,12 @@ class TestAXElement < MiniTest::Unit::TestCase
|
|
38
42
|
def center_test position, size, expected
|
39
43
|
element.define_singleton_method :attribute do |attr|
|
40
44
|
case attr
|
41
|
-
when :position then
|
42
|
-
when :size then
|
45
|
+
when :position then CGPoint.new(*position)
|
46
|
+
when :size then CGSize.new(*size)
|
43
47
|
else raise ArgumentError
|
44
48
|
end
|
45
49
|
end
|
46
|
-
assert_equal
|
50
|
+
assert_equal CGPoint.new(*expected), element.to_point
|
47
51
|
end
|
48
52
|
|
49
53
|
# the nil case
|
@@ -7,9 +7,11 @@ class TestMiniTestAssertions < MiniTest::Unit::TestCase
|
|
7
7
|
assert_respond_to self, :assert_has_child
|
8
8
|
assert_respond_to self, :assert_has_descendent
|
9
9
|
assert_respond_to self, :assert_has_descendant
|
10
|
+
assert_respond_to self, :assert_shortly_has
|
10
11
|
assert_respond_to self, :refute_has_child
|
11
12
|
assert_respond_to self, :refute_has_descendent
|
12
13
|
assert_respond_to self, :refute_has_descendant
|
14
|
+
assert_respond_to self, :refute_shortly_has
|
13
15
|
end
|
14
16
|
|
15
17
|
end
|
@@ -7,6 +7,9 @@ class TestRSpecMatchers < MiniTest::Unit::TestCase
|
|
7
7
|
assert TopLevel.method(:have_child)
|
8
8
|
assert TopLevel.method(:have_descendent)
|
9
9
|
assert TopLevel.method(:have_descendant)
|
10
|
+
assert TopLevel.method(:shortly_have_child)
|
11
|
+
assert TopLevel.method(:shortly_have_descendent)
|
12
|
+
assert TopLevel.method(:shortly_have_descendant)
|
10
13
|
end
|
11
14
|
|
12
15
|
end
|
data/test/sanity/test_mouse.rb
CHANGED
@@ -7,11 +7,11 @@ class TestMouseModule < MiniTest::Unit::TestCase
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def test_move_to
|
10
|
-
point =
|
10
|
+
point = CGPoint.new(100, 100)
|
11
11
|
Mouse.move_to point
|
12
12
|
assert_in_delta 0, distance(point,Mouse.current_position), 1.0
|
13
13
|
|
14
|
-
point =
|
14
|
+
point = CGPoint.new(rand(700)+150, rand(500)+100)
|
15
15
|
Mouse.move_to point
|
16
16
|
assert_in_delta 0, distance(point,Mouse.current_position), 1.0
|
17
17
|
end
|
metadata
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
name: AXElements
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.8.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Mark Rada
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-04-
|
12
|
+
date: 2012-04-24 00:00:00 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|
@@ -70,30 +70,15 @@ email: mrada@marketcircle.com
|
|
70
70
|
executables: []
|
71
71
|
extensions:
|
72
72
|
- ext/accessibility/key_coder/extconf.rb
|
73
|
-
extra_rdoc_files:
|
74
|
-
- docs/AccessibilityTips.markdown
|
75
|
-
- docs/Acting.markdown
|
76
|
-
- docs/Debugging.markdown
|
77
|
-
- docs/images/all_the_buttons.jpg
|
78
|
-
- docs/images/next_version.png
|
79
|
-
- docs/images/ui_hierarchy.dot
|
80
|
-
- docs/images/ui_hierarchy.png
|
81
|
-
- docs/Inspecting.markdown
|
82
|
-
- docs/KeyboardEvents.markdown
|
83
|
-
- docs/NewBehaviour.markdown
|
84
|
-
- docs/Notifications.markdown
|
85
|
-
- docs/Searching.markdown
|
86
|
-
- docs/TestingExtensions.markdown
|
87
|
-
- .yardopts
|
88
|
-
- README.markdown
|
73
|
+
extra_rdoc_files: []
|
89
74
|
files:
|
90
75
|
- lib/accessibility/core.rb
|
91
|
-
- lib/accessibility/debug.rb
|
92
76
|
- lib/accessibility/dsl.rb
|
93
77
|
- lib/accessibility/enumerators.rb
|
94
78
|
- lib/accessibility/errors.rb
|
95
79
|
- lib/accessibility/factory.rb
|
96
80
|
- lib/accessibility/graph.rb
|
81
|
+
- lib/accessibility/highlighter.rb
|
97
82
|
- lib/accessibility/pp_inspector.rb
|
98
83
|
- lib/accessibility/qualifier.rb
|
99
84
|
- lib/accessibility/string.rb
|
@@ -103,6 +88,7 @@ files:
|
|
103
88
|
- lib/ax/application.rb
|
104
89
|
- lib/ax/button.rb
|
105
90
|
- lib/ax/element.rb
|
91
|
+
- lib/ax/pop_up_button.rb
|
106
92
|
- lib/ax/radio_button.rb
|
107
93
|
- lib/ax/row.rb
|
108
94
|
- lib/ax/static_text.rb
|
@@ -125,8 +111,9 @@ files:
|
|
125
111
|
- rakelib/gem.rake
|
126
112
|
- rakelib/test.rake
|
127
113
|
- Rakefile
|
114
|
+
- README.markdown
|
115
|
+
- .yardopts
|
128
116
|
- test/integration/accessibility/test_core.rb
|
129
|
-
- test/integration/accessibility/test_debug.rb
|
130
117
|
- test/integration/accessibility/test_dsl.rb
|
131
118
|
- test/integration/accessibility/test_enumerators.rb
|
132
119
|
- test/integration/accessibility/test_errors.rb
|
@@ -138,10 +125,10 @@ files:
|
|
138
125
|
- test/integration/minitest/test_ax_elements.rb
|
139
126
|
- test/integration/rspec/expectations/test_ax_elements.rb
|
140
127
|
- test/sanity/accessibility/test_core.rb
|
141
|
-
- test/sanity/accessibility/test_debug.rb
|
142
128
|
- test/sanity/accessibility/test_dsl.rb
|
143
129
|
- test/sanity/accessibility/test_errors.rb
|
144
130
|
- test/sanity/accessibility/test_factory.rb
|
131
|
+
- test/sanity/accessibility/test_highlighter.rb
|
145
132
|
- test/sanity/accessibility/test_pp_inspector.rb
|
146
133
|
- test/sanity/accessibility/test_qualifier.rb
|
147
134
|
- test/sanity/accessibility/test_string.rb
|
@@ -153,24 +140,10 @@ files:
|
|
153
140
|
- test/sanity/ax_elements/test_nsobject_inspect.rb
|
154
141
|
- test/sanity/minitest/test_ax_elements.rb
|
155
142
|
- test/sanity/rspec/expectations/test_ax_elements.rb
|
143
|
+
- test/sanity/test_accessibility.rb
|
156
144
|
- test/sanity/test_ax_elements.rb
|
157
145
|
- test/sanity/test_mouse.rb
|
158
146
|
- test/helper.rb
|
159
|
-
- docs/AccessibilityTips.markdown
|
160
|
-
- docs/Acting.markdown
|
161
|
-
- docs/Debugging.markdown
|
162
|
-
- docs/images/all_the_buttons.jpg
|
163
|
-
- docs/images/next_version.png
|
164
|
-
- docs/images/ui_hierarchy.dot
|
165
|
-
- docs/images/ui_hierarchy.png
|
166
|
-
- docs/Inspecting.markdown
|
167
|
-
- docs/KeyboardEvents.markdown
|
168
|
-
- docs/NewBehaviour.markdown
|
169
|
-
- docs/Notifications.markdown
|
170
|
-
- docs/Searching.markdown
|
171
|
-
- docs/TestingExtensions.markdown
|
172
|
-
- .yardopts
|
173
|
-
- README.markdown
|
174
147
|
homepage: http://github.com/Marketcircle/AXElements
|
175
148
|
licenses:
|
176
149
|
- BSD 3-clause
|
@@ -198,7 +171,6 @@ specification_version: 3
|
|
198
171
|
summary: A DSL for automating GUI manipulation
|
199
172
|
test_files:
|
200
173
|
- test/integration/accessibility/test_core.rb
|
201
|
-
- test/integration/accessibility/test_debug.rb
|
202
174
|
- test/integration/accessibility/test_dsl.rb
|
203
175
|
- test/integration/accessibility/test_enumerators.rb
|
204
176
|
- test/integration/accessibility/test_errors.rb
|
@@ -210,10 +182,10 @@ test_files:
|
|
210
182
|
- test/integration/minitest/test_ax_elements.rb
|
211
183
|
- test/integration/rspec/expectations/test_ax_elements.rb
|
212
184
|
- test/sanity/accessibility/test_core.rb
|
213
|
-
- test/sanity/accessibility/test_debug.rb
|
214
185
|
- test/sanity/accessibility/test_dsl.rb
|
215
186
|
- test/sanity/accessibility/test_errors.rb
|
216
187
|
- test/sanity/accessibility/test_factory.rb
|
188
|
+
- test/sanity/accessibility/test_highlighter.rb
|
217
189
|
- test/sanity/accessibility/test_pp_inspector.rb
|
218
190
|
- test/sanity/accessibility/test_qualifier.rb
|
219
191
|
- test/sanity/accessibility/test_string.rb
|
@@ -225,6 +197,7 @@ test_files:
|
|
225
197
|
- test/sanity/ax_elements/test_nsobject_inspect.rb
|
226
198
|
- test/sanity/minitest/test_ax_elements.rb
|
227
199
|
- test/sanity/rspec/expectations/test_ax_elements.rb
|
200
|
+
- test/sanity/test_accessibility.rb
|
228
201
|
- test/sanity/test_ax_elements.rb
|
229
202
|
- test/sanity/test_mouse.rb
|
230
203
|
- test/helper.rb
|
@@ -1,119 +0,0 @@
|
|
1
|
-
# Accessibility Tips
|
2
|
-
|
3
|
-
This document includes tips for customizing accessibility in your own
|
4
|
-
applications. The goal is to inform you of pitfalls that are not
|
5
|
-
mentioned in Apple's documentation. It also includes notes on how to
|
6
|
-
avoid making decsions that will make an application incompatible with
|
7
|
-
AXElements.
|
8
|
-
|
9
|
-
@todo This document is under construction and is currently just a set
|
10
|
-
of notes and unorganized sections ripped from other documents.
|
11
|
-
|
12
|
-
## Guidelines
|
13
|
-
|
14
|
-
When implementing custom accessibility roles, you should never use a
|
15
|
-
pluralized name for the role. A role that is already pluralized will
|
16
|
-
break search.
|
17
|
-
|
18
|
-
|
19
|
-
## Adding new stuff
|
20
|
-
|
21
|
-
Adding new attributes to an object is very simple...usually. Most of
|
22
|
-
the time it is as simple as overriding two methods:
|
23
|
-
|
24
|
-
- `accessibilityAttributeNames`
|
25
|
-
- `accessibilityAttributeValue:`
|
26
|
-
|
27
|
-
`accessibilityAttributeNames` needs to return an array with the names
|
28
|
-
of available attributes. You should call `super` to get the existing
|
29
|
-
array first, and then append any custom attributes you want to
|
30
|
-
provide.
|
31
|
-
|
32
|
-
An attribute name must follow the convention of being a camel cased
|
33
|
-
string with the "AX" prefix, such as `AXTitle`. You can optionally
|
34
|
-
include an additional prefix before "AX", such as "MCAX" for
|
35
|
-
Marketcircle custom attributes. If you do not follow these rules then
|
36
|
-
attribute names will not be translated properly by AXElements.
|
37
|
-
|
38
|
-
`accessibilityAttributeValue:` is how you actually provide the value
|
39
|
-
for the attribute; the parameter for this method is the name of the
|
40
|
-
attribute fetched from calling `accessibilityAttributeNames`. You
|
41
|
-
should return the value without doing any extra work to the data; the
|
42
|
-
Accessibility interface will do any wrapping or translating for
|
43
|
-
you (e.g. CGPoint objects will have co-ordinates flipped). In the case
|
44
|
-
of a C structs you should leave them wrapped in an NSValue object.
|
45
|
-
|
46
|
-
Similarly, parameterized attributes are added using methods with
|
47
|
-
`Parameter` in the name. The method for getting the value of a
|
48
|
-
parameterized attribute will of course take an extra parameter for the
|
49
|
-
parameterized attributes parameter.
|
50
|
-
|
51
|
-
### Where To Implement The Methods
|
52
|
-
|
53
|
-
The difficulty in adding attributes lies in finding out where you need
|
54
|
-
to add the accessibility methods. Often, the class that implements
|
55
|
-
accessibility is the class that draws the user interface element. For
|
56
|
-
example, the accessibility information for a button is implemented on
|
57
|
-
the button cell and not the button itself. Since Apple is under the
|
58
|
-
delusion that you will never need to add any custom accessibility
|
59
|
-
information, they really don't document these customizations enough,
|
60
|
-
and so this document only includes caveats that have been discovered
|
61
|
-
so far.
|
62
|
-
|
63
|
-
It is often inconvenient to subclass the cell for an object just for
|
64
|
-
accessibility. In these cases Apple has provided something similar to
|
65
|
-
singleton methods, but less useful, with the
|
66
|
-
`accessibilitySetOverrideValue:forAttribute:` method. You can use the
|
67
|
-
method to override an attribute or even add a new one. The main issue
|
68
|
-
with this method is that any attribute that is overridden, or added,
|
69
|
-
will not be writable using the accessibility APIs. Another issue is
|
70
|
-
that you have to calculate the value when you override instead of when
|
71
|
-
the attribute value is queried by the client.
|
72
|
-
|
73
|
-
### Writability
|
74
|
-
|
75
|
-
In the case where it is convenient to be able to change an attribute's
|
76
|
-
value through accessibility, like the size of a window, you will also
|
77
|
-
need to implement two more methods for the attribute:
|
78
|
-
|
79
|
-
- `accessibilityIsAttributeSettable:`
|
80
|
-
- `accessibilitySetValue:forAttribute:`
|
81
|
-
|
82
|
-
`accessibilityIsAttributeSettable:` simply responds with whether or
|
83
|
-
not the attribute is writable, and
|
84
|
-
`accessibilitySetValue:forAttribute:` will be called to actually write
|
85
|
-
to the attribute.
|
86
|
-
|
87
|
-
### Remember `super`
|
88
|
-
|
89
|
-
It is important to remember that these methods are already implemented
|
90
|
-
for the built in features. When overriding, you should only implement
|
91
|
-
the custom behaviour and call `super` to handle everything else.
|
92
|
-
|
93
|
-
### Existing Definitions
|
94
|
-
|
95
|
-
When implementing custom behaviour, you should try and use
|
96
|
-
pseudoclasses that have already been defined by Apple, as well as
|
97
|
-
attributes, parameterized attributes, and other features that have
|
98
|
-
already been defined. Apple maintains the documentation for all their
|
99
|
-
definitions on
|
100
|
-
[here](http://developer.apple.com/library/mac/#documentation/UserExperience/Reference/Accessibility_RoleAttribute_Ref/Introduction.html#//apple_ref/doc/uid/TP40007870).
|
101
|
-
The documentation is fairly detailed now (moreso than before), but
|
102
|
-
still misses a few things.
|
103
|
-
|
104
|
-
|
105
|
-
## Adding Accessibility Actions
|
106
|
-
|
107
|
-
If you have access to the source code for an app, you can add more
|
108
|
-
accessibility actions. You need to override two methods, just like
|
109
|
-
with adding attributes:
|
110
|
-
|
111
|
-
– `accessibilityActionNames`
|
112
|
-
– `accessibilityPerformAction:`
|
113
|
-
|
114
|
-
These methods should be implemented in the same way that you would
|
115
|
-
implement new attributes, just as detailed in the
|
116
|
-
{file:docs/Inspecting.markdown Inspecting tutorial}. The one
|
117
|
-
difference is that `accessibilityPerformAction:` should not return
|
118
|
-
anything after it performs the action.
|
119
|
-
|
data/docs/Acting.markdown
DELETED
@@ -1,340 +0,0 @@
|
|
1
|
-
# Actions (a.k.a. The Fun Part)
|
2
|
-
|
3
|
-
Actions are how you drive the user interface. They can be anything
|
4
|
-
from showing a menu to dragging a window, but are often simple things
|
5
|
-
like pressing a button. Most actions are subtly different cases
|
6
|
-
of the same thing, but it is important to understand the differences
|
7
|
-
and when to use different actions.
|
8
|
-
|
9
|
-
There are three types of actions, those provided by the accessibility
|
10
|
-
API as direct calls, those provided by accessibility APIs as keyboard
|
11
|
-
simulation, and those that are provided by the CGEvents API. In most
|
12
|
-
cases you can use at least two types of actions to achieve the desired
|
13
|
-
effect and each type of action has its own pros and cons to consider.
|
14
|
-
|
15
|
-
## Interface
|
16
|
-
|
17
|
-
Though there are three different APIs under the hood, they are exposed
|
18
|
-
through a common interface via the DSL methods in
|
19
|
-
{Accessibility::Language}.
|
20
|
-
|
21
|
-
app = Accessibility.application_with_name 'Terminal'
|
22
|
-
press app.main_window.minimize_button
|
23
|
-
type "\\CMD+M", app
|
24
|
-
click app.main_window.minimize_button
|
25
|
-
|
26
|
-
The above example demonstrates performing actions using the DSL
|
27
|
-
methods. The DSL methods are always some kind of action, such as
|
28
|
-
{Accessibility::Language#drag_mouse_to drag\_mouse\_to}, or
|
29
|
-
{Accessibility::Language#set_focus set\_focus} to name two.
|
30
|
-
|
31
|
-
The point of the DSL is to put the actions at the front of statements
|
32
|
-
in order to separate verbs from nouns. In this way, scripts should
|
33
|
-
resemble instructions that you might give for reproduction steps in a
|
34
|
-
bug report---they should read like they were instructions for
|
35
|
-
a person. This style should make it easier to think about writing
|
36
|
-
scripts and it should also give better hints at when to refactor
|
37
|
-
code into helper methods like `log_in_as:with_password:`.
|
38
|
-
|
39
|
-
If you tried out the code snippet above you might notice the
|
40
|
-
difference between the APIs (you also might notice that the second
|
41
|
-
command does not work, it is a work in progress :(). In the first
|
42
|
-
case, `press` was using the actions provided by the accessibility APIs
|
43
|
-
to call the press action on the minimize button for the window, in the
|
44
|
-
second case we used `type` to type in the hot key for minimizing a
|
45
|
-
window, and in the third case we used `click` from the CGEvents APIs
|
46
|
-
to actually move the mouse cursor to the button and generate a click
|
47
|
-
event.
|
48
|
-
|
49
|
-
## Accessibility Actions
|
50
|
-
|
51
|
-
Actions like `press` use the accessibility APIs and show up in the
|
52
|
-
list of actions returned by {AX::Element#actions}. These actions will
|
53
|
-
also show up in the Accessibility Inspector in the Actions section.
|
54
|
-
|
55
|
-
You may have noticed that {Accessibility::Language#press press}
|
56
|
-
shows up as `AXPress` for buttons when you view them with the
|
57
|
-
Accessibility Inspector or by calling {AX::Element#actions}. This is
|
58
|
-
because we do name translation with accessibility actions just like
|
59
|
-
when accessing attributes as shown in the
|
60
|
-
{file:docs/Inspecting.markdown Inspection tutorial}.
|
61
|
-
|
62
|
-
Accessibility actions are the simplest and fastest way to trigger
|
63
|
-
actions. They use the accessibility APIs to talk directly to the
|
64
|
-
application to call the same methods that would be called when
|
65
|
-
you interact with the application with the mouse or keyboard, but
|
66
|
-
without having to use the mouse or keyboard.
|
67
|
-
|
68
|
-
## Setting Attributes
|
69
|
-
|
70
|
-
Though it is not really an action, some attributes can have their
|
71
|
-
values changed through the accessibility APIs. The
|
72
|
-
{Accessibility::Language} module has two methods, `set` and
|
73
|
-
`set_focus`, which allow you to change the value for certain
|
74
|
-
attributes.
|
75
|
-
|
76
|
-
In the Accessibility Inspector, a writable attribute will have `(W)`
|
77
|
-
next to the attribute name, and the programmatic way to tell if an
|
78
|
-
attribute is writable is to call {AX::Element#attribute_writable?} and
|
79
|
-
pass the name of the attribute as a parameter:
|
80
|
-
|
81
|
-
app.main_window.attribute_writable? :size
|
82
|
-
app.main_window.attribute_writable? :title
|
83
|
-
|
84
|
-
You can only set an attribute if it is writable. When you have an
|
85
|
-
attribute that is writable, and you would like to change it, then you
|
86
|
-
simply need to call `set`:
|
87
|
-
|
88
|
-
set app.main_window, size: [500, 500].to_size
|
89
|
-
|
90
|
-
The first parameter is the element, and the second parameter is a
|
91
|
-
key-value pair with the attribute as the key and the value as the
|
92
|
-
new value.
|
93
|
-
|
94
|
-
`set_focus` is just syntactic sugar for `set` where the key-value is
|
95
|
-
set for you and the only parameter you provide is the element you want
|
96
|
-
to set focus to:
|
97
|
-
|
98
|
-
set_focus app.main_window.text_field
|
99
|
-
|
100
|
-
Though, `set` itself has a special case. The second most frequently
|
101
|
-
changed attribute for a element is the `value`, it is used almost as
|
102
|
-
much as `focused`. For this case, you can pass anything that is not a
|
103
|
-
key-value pair to `set` and it will be assumed that you want to change
|
104
|
-
the value of the `value` attribute:
|
105
|
-
|
106
|
-
set app.main_window.text_field, 'Mark Rada'
|
107
|
-
|
108
|
-
Another important detail that you might be curious about is that we
|
109
|
-
called `to_size` on the first snippet showing how to set an
|
110
|
-
attribute. The developers of MacRuby have added a feature that allows
|
111
|
-
you to pass an array as an argument when you would normally be
|
112
|
-
expected to pass a structure such as a `CGPoint`. Since AXElements
|
113
|
-
APIs inherently need to handle any type of object, it is not sane to try
|
114
|
-
and do the same type of run time type analysis. In its place, I have
|
115
|
-
provided some convenience methods for {NSArray arrays} to let you
|
116
|
-
quickly transform them to the appropriate type of object. In my
|
117
|
-
opinion, you should just use the proper type of object in the first
|
118
|
-
place and avoid the overhead.
|
119
|
-
|
120
|
-
### You Want To Use Actions
|
121
|
-
|
122
|
-
The advantage to using accessibility actions is that they are usually
|
123
|
-
much easier to use than the other APIs. Accessibility actions are
|
124
|
-
usually synchronous and so your scripts will not need to worry about
|
125
|
-
waiting for animations to complete. The only complication is that
|
126
|
-
sometimes an action is not synchronous and there is no good way to
|
127
|
-
tell if an action is synchronous without looking at the underlying
|
128
|
-
code.
|
129
|
-
|
130
|
-
### Asynchronous Problems
|
131
|
-
|
132
|
-
Problems with synchronous behaviour will occur with all the types of
|
133
|
-
actions, but less often with accessibility actions, so you should
|
134
|
-
always try thing out first in the console to make sure. A good rule of
|
135
|
-
thumb is that anything that looks like it would take a noticeable
|
136
|
-
amount of time, and is not an animation, will probably be
|
137
|
-
asynchronous. An example of this would be having to load data from a
|
138
|
-
database (read: separate process, across a network, etc.) such as in
|
139
|
-
the Marketcircle server admin apps or the Billings Pro preferences.
|
140
|
-
|
141
|
-
Asynchronous behaviour can often be worked around by using
|
142
|
-
accessibility {file:docs/Notifications.markdown notifications} or by
|
143
|
-
simply calling
|
144
|
-
[`sleep`](http://rdoc.info/stdlib/core/1.9.2/Kernel#sleep-instance_method).
|
145
|
-
Notifications have some interesting caveats that are covered in their
|
146
|
-
own tutorial and so it is much easier to `sleep`.
|
147
|
-
|
148
|
-
However, using `sleep` to solve the problem of asynchronous waiting is
|
149
|
-
like using a steak knife for brain surgery. Unless you can control the
|
150
|
-
environment when the script is running, you will need to sleep for
|
151
|
-
longer periods of time than you really need to; even when you have
|
152
|
-
quite a bit of control you might still have the occasional instance
|
153
|
-
where a database fetch takes longer than expected. I would suggest you
|
154
|
-
use notifications when you can, and sleep when you cannot.
|
155
|
-
|
156
|
-
## CGEvents Actions
|
157
|
-
|
158
|
-
CGEvents based actions never directly use the accessibility APIs and
|
159
|
-
are implemented at a different level of the GUI stack in OS X (I
|
160
|
-
assume). An action that uses CGEvents will actually move the mouse
|
161
|
-
cursor around and simulate mouse input at a relatively low level of
|
162
|
-
abstraction. However, accessibility information is still used to find
|
163
|
-
the point on the screen to move to and click. These types of actions
|
164
|
-
are more realistic, more awesome looking, and also more difficult to
|
165
|
-
write.
|
166
|
-
|
167
|
-
The difficulty in writing the scripts comes from the fact that it does
|
168
|
-
not directly communicate with the accessibility APIs. This implicitly
|
169
|
-
means that all CGEvents actions are asynchronous, which is the only
|
170
|
-
non-trivial complication with using CGEvents APIs.
|
171
|
-
|
172
|
-
### CGEvents Goes Where Accessibility Cannot
|
173
|
-
|
174
|
-
The benefit of actually moving the mouse will often outweigh the
|
175
|
-
downside here. Moving the mouse cursor and generating click events
|
176
|
-
allows things like dragging and scrolling which cannot be done using
|
177
|
-
the accessibility APIs.
|
178
|
-
|
179
|
-
As mentioned earlier, CGEvents doesn't talk to applications in the
|
180
|
-
same way that accessibility APIs do, which means that you can use
|
181
|
-
CGEvents actions on elements that do not fully support
|
182
|
-
accessibility. For instance, text might appear on the UI and have a
|
183
|
-
`click`-able link but may not provide an equivalent action to clicking
|
184
|
-
on the link; using CGEvents APIs you will only need to move to the
|
185
|
-
position of the text and then generate a click event.
|
186
|
-
|
187
|
-
### Realism
|
188
|
-
|
189
|
-
Since CGEvents actually manipulates the mouse cursor, any script that
|
190
|
-
uses the APIs will be more realistic than their equivalent
|
191
|
-
accessibility actions. This can make a difference if you are testing
|
192
|
-
an app and you are expecting other side effects to occur. For instance,
|
193
|
-
what if you implemented a sort of UI element that requires the mouse
|
194
|
-
cursor to be near the element for an action to trigger, such as an
|
195
|
-
expanding drawer or a folder in the finder. Depending on what you are
|
196
|
-
using AXElements for, this may or may not be important to you.
|
197
|
-
|
198
|
-
### Underpinnings
|
199
|
-
|
200
|
-
An important thing to note is that AXElements always works with
|
201
|
-
flipped co-ordinates. The origin, `(0, 0)` is always located in the
|
202
|
-
top left corner of the main display. Displays to the left have a
|
203
|
-
negative `x` co-ordinate and displays above the main display will have
|
204
|
-
negative `y` co-ordinates.
|
205
|
-
|
206
|
-
Though the CGEvents APIs are exposed to AXElements through the
|
207
|
-
{Accessibility::Language Language} module, there is another thin
|
208
|
-
layer between AXElements and CGEvents that abstracts everything that
|
209
|
-
AXElements uses.
|
210
|
-
|
211
|
-
The between layer is the {Mouse} module, and it is responsible for
|
212
|
-
generating events and animating the mouse movements on screen. The API
|
213
|
-
that it makes available to AXElements is more fine grained than what
|
214
|
-
AXElements makes available to you. There is a lot of room for the
|
215
|
-
{Mouse} module to grow and add new features, some of which are noted
|
216
|
-
as `@todo` items in the documentation. It may also be necessary in
|
217
|
-
the future to make available from AXElements certain options that are
|
218
|
-
normally hidden in {Mouse}.
|
219
|
-
|
220
|
-
### Where The Mouse Moves
|
221
|
-
|
222
|
-
Consider the following:
|
223
|
-
|
224
|
-
move_mouse_to app.main_window.close_button
|
225
|
-
|
226
|
-
If you try out that code snippet then you will notice that the mouse
|
227
|
-
cursor moves to the button, specifically the center of the button. If
|
228
|
-
you use the method to move to another object, such as a button, you
|
229
|
-
will again notice that it moved to the center point of the element.
|
230
|
-
|
231
|
-
You can call `move_mouse_to` and give it a UI element, but you could
|
232
|
-
also be more exact and give it a CGPoint or even an array with two
|
233
|
-
numbers. In fact, you could pass just any object as long it implements
|
234
|
-
a method named `to_point` which returns a CGPoint object. If you were
|
235
|
-
to look at the code for {Accessibility::Language#move_mouse_to} you
|
236
|
-
would see that all it does is call `#to_point` on the argument and
|
237
|
-
then pass the returned value to the {Mouse} module to actually do the
|
238
|
-
work. You could also look at {NSArray#to_point} and find out that it just
|
239
|
-
returns a new CGPoint using the first two objects in the array, and
|
240
|
-
`CGPoint#to_point` just returns itself (you can't see the
|
241
|
-
documentation for the method because of limitations in
|
242
|
-
YARD). Similarly, {AX::Element} implements
|
243
|
-
{AX::Element#to_point to_point} which not only gets the position for
|
244
|
-
the element, but also gets the size and then calculates the center
|
245
|
-
point for the element. This is important because you probably never
|
246
|
-
want the mouse cursor moving to the top left of the UI element, but
|
247
|
-
maybe you don't want the cursor moving to the center either.
|
248
|
-
|
249
|
-
If you want to have the mouse move to a location over a UI element
|
250
|
-
that is not the center point, then you will need to implement
|
251
|
-
`to_point` for the appropriate class. Just remember to follow the
|
252
|
-
{file:docs/NewBehaviour.markdown rules} for choosing the proper
|
253
|
-
superclasss. For instance, on Snow Leopard you could resize a window
|
254
|
-
by clicking and dragging an `AX::GrowArea` object in the lower right
|
255
|
-
corner of a window; moving to the center of the element may not
|
256
|
-
actually allow the mouse to respond and so you would have to implement
|
257
|
-
`to_point` to move closer to the bottom right of the window
|
258
|
-
(__NOTE__: I don't know if that is actually true for `AX::GrowArea`,
|
259
|
-
it was just meant as an example).
|
260
|
-
|
261
|
-
### Dragging
|
262
|
-
|
263
|
-
Sometimes you just need to be able to click and drag an element. To do
|
264
|
-
this you can only use the CGEvents APIs. Fortunately, click and drag
|
265
|
-
is relatively painless with AXElements, the simplest example would be
|
266
|
-
something like this:
|
267
|
-
|
268
|
-
# move to the mouse to the starting point
|
269
|
-
move_mouse_to app.main_window.title_ui_element
|
270
|
-
|
271
|
-
# start the click and drag event
|
272
|
-
drag_mouse_to [0, 0]
|
273
|
-
|
274
|
-
Pretty cool, eh? The general pattern is to move the mouse to the
|
275
|
-
starting point, and then to call `drag_mouse_to` to start the click
|
276
|
-
and drag events. In the example, I gave co-ordinates using an array
|
277
|
-
with two numbers but you can pass a CGPoint or anything else that
|
278
|
-
responds to `to_point` just like with the other CGEvents actions.
|
279
|
-
|
280
|
-
## Keyboard Actions
|
281
|
-
|
282
|
-
The final type of action is the keyboard action. Keyboard actions
|
283
|
-
live in a world between the accessibility APIs and CGEvent APIs; that
|
284
|
-
is, Apple has already done the work of unifying them for me and in a
|
285
|
-
way that I would not have been able to do myself.
|
286
|
-
|
287
|
-
The keyboard action is a single method,
|
288
|
-
{Accessibility::Language#type}, that requires one parameter and
|
289
|
-
takes an optional second parameter. The first parameter is simply a
|
290
|
-
string that includes which characters you wish to have typed out and
|
291
|
-
the second parameter is the UI element for the application where you
|
292
|
-
would like to send the keyboard events. Since the second parameter is
|
293
|
-
optional, the currently focused app will receive the events if the
|
294
|
-
parameter is not used.
|
295
|
-
|
296
|
-
# to a specific app
|
297
|
-
app = Accessibility.application_with_name 'Terminal'
|
298
|
-
type "Hello, world!", app
|
299
|
-
|
300
|
-
# to the focused app
|
301
|
-
type "Hello, world!"
|
302
|
-
|
303
|
-
The string that you pass can have some special escape sequences to
|
304
|
-
indicate that you want to press command characters or a combination of
|
305
|
-
keys at once for a hot key. Details on this are contained in their own
|
306
|
-
tutorial, the
|
307
|
-
{file:docs/KeyboardEvents.markdown Keyboard Events Tutorial}.
|
308
|
-
|
309
|
-
## Mixing In
|
310
|
-
|
311
|
-
By now you will have noticed that all the actions have been defined in
|
312
|
-
the {Accessibility::Language} name space, which is just a simple
|
313
|
-
module. Though, you have been able to use the methods anywhere and in
|
314
|
-
any context.
|
315
|
-
|
316
|
-
When you load AXElements, by using `require 'ax_elements'`, not only
|
317
|
-
will all the code be loaded, but the extra step of mixing
|
318
|
-
{Accessibility::Language} into the top level name space will also be
|
319
|
-
done for you. The only way to override this behaviour is to load the
|
320
|
-
components of AXElements yourself, the details on how to do this are
|
321
|
-
left as an exercise to interested parties. :P
|
322
|
-
|
323
|
-
## A Note About Caching
|
324
|
-
|
325
|
-
You need to be careful when you cache UI element objects. Every time
|
326
|
-
that you trigger an action you are intrinsically changing the state of
|
327
|
-
an application. State changes will often cause new UI elements to be
|
328
|
-
created, recreated, or removed.
|
329
|
-
|
330
|
-
For example consider pressing the close button for a window; in this
|
331
|
-
case, an entire window and all of its children will disappear and
|
332
|
-
become invalid objects. Another case might be pressing the `+` button
|
333
|
-
for a table; in this case you have created a new row for the table and
|
334
|
-
any cache of the existing rows for the table will not include the new
|
335
|
-
element.
|
336
|
-
|
337
|
-
The real problem with caching is with the invalid objects. An invalid
|
338
|
-
object is poisonous to the MacRuby run time. If you try to access an
|
339
|
-
attribute or trigger a search from an invalid then you will cause
|
340
|
-
MacRuby to crash.
|