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,271 @@
|
|
1
|
+
# Notifications
|
2
|
+
|
3
|
+
@todo This document needs to be audited for accuracy. The interface
|
4
|
+
for notifications is in flux and this document might be out of sync
|
5
|
+
with the current implementation.
|
6
|
+
|
7
|
+
Accessibility notifications are a tool that you can use to add a delay
|
8
|
+
in script that waits until a certain event happens. These events could
|
9
|
+
be a window being created, a menu being opened, or one of a plethora
|
10
|
+
of other events.
|
11
|
+
|
12
|
+
A notification is much more time efficient than simply using `sleep`
|
13
|
+
to estimate how long an action will take to complete. Sleep often
|
14
|
+
causes you to wait longer than is necessary to avoid waiting less time
|
15
|
+
than what is needed. With notifications you set a maximum amount of
|
16
|
+
time that should be waited, called the timeout, but waiting is stopped
|
17
|
+
as soon as the notification is received. This makes using
|
18
|
+
notifications preferable to simply sleeping.
|
19
|
+
|
20
|
+
An important thing to understand is that an accessibility notification
|
21
|
+
is separate from the notifications that are sent and received
|
22
|
+
internally in a Cocoa app. Accessibility notifications are not very
|
23
|
+
different in concept, they differ largely in mechanics. This is
|
24
|
+
because talking to another application,
|
25
|
+
[across processes](http://en.wikipedia.org/wiki/Inter-process_communication),
|
26
|
+
is expensive and would be impractical to do for every notification
|
27
|
+
that is sent internally in an application. This makes a bit more
|
28
|
+
difficult to track down what notification is going to be sent through
|
29
|
+
accessibility; using notifications more difficult than simply using
|
30
|
+
`sleep`, at least until you have a lot of practice.
|
31
|
+
|
32
|
+
## Simple Example
|
33
|
+
|
34
|
+
Consider a log in window. You enter some credentials and then you
|
35
|
+
press a button to log in and load up the main application window.
|
36
|
+
This procedure is most likely asynchronous, it usually takes one or
|
37
|
+
two seconds, but could take longer. To script logging in you would
|
38
|
+
have to wait until the main application window loaded. You could just
|
39
|
+
use `Kernel#sleep`, but how long should you sleep for and how reliable
|
40
|
+
is that? How much time would that waste?
|
41
|
+
|
42
|
+
Notifications are perfect for this scenario; whenever a window is
|
43
|
+
created, the application sends out a notification to any accessibility
|
44
|
+
applications that are listening letting them know a window was
|
45
|
+
created; the notification will even include the object that sent the
|
46
|
+
notification and the name of the notification. Using this system, we
|
47
|
+
can just wait for the notification to be received and continue
|
48
|
+
executing the script right away (technically, there can be few
|
49
|
+
milliseconds of delay). A typical example of using notifications looks
|
50
|
+
like this:
|
51
|
+
|
52
|
+
register_for_notification app, :window_created
|
53
|
+
login # trigger an action that will eventually send a notification
|
54
|
+
wait_for_notification
|
55
|
+
|
56
|
+
And that's it.
|
57
|
+
|
58
|
+
## One-Two-Three Combo
|
59
|
+
|
60
|
+
Using notifications is a three step process: first a left jab to set up
|
61
|
+
the notification, then you dodge the counter punch, and then you
|
62
|
+
finish with a right cross to wait for the notification. If you're left
|
63
|
+
handed then you can jab with the right and cross with the left (but
|
64
|
+
you still setup first and then wait).
|
65
|
+
|
66
|
+
### First You Set It Up
|
67
|
+
|
68
|
+
Setup is the important step and is fairly painless as far as what
|
69
|
+
you need to provide for AXElements. To register for a notification you
|
70
|
+
call {Accessibility::Language#register_for_notification}. You pass the
|
71
|
+
the {AX::Element} who will be sending the notification and then the
|
72
|
+
notification name. Another simple example would be:
|
73
|
+
|
74
|
+
register_for_notification text_field, :value_changed
|
75
|
+
|
76
|
+
In this hypothetical case, we want to wait for a `:value_changed`
|
77
|
+
notification that will be sent by `text_field`. That's it. And that
|
78
|
+
also covers 90% of the cases where you will use notifications.
|
79
|
+
|
80
|
+
#### Applications Are Special
|
81
|
+
|
82
|
+
Listening from the application, an {AX::Application}, object is a
|
83
|
+
special case. When you say that an {AX::Application} object will be
|
84
|
+
the sender, what you are saying is that any UI element in the app will
|
85
|
+
be the sender and you don't really care as long as you get the right
|
86
|
+
notification name (that's not entirely true, but a good enough lie for
|
87
|
+
now).
|
88
|
+
|
89
|
+
#### Notification Names or: How Is Babby Formed?
|
90
|
+
|
91
|
+
Like attribute and action name translation, notification names are
|
92
|
+
also translated for your convenience. However, where do you get the
|
93
|
+
notification names that Apple provides?
|
94
|
+
|
95
|
+
It turns out that there is no API for getting a list of notifications
|
96
|
+
that an element can send---Apple dropped the ball in this case, but
|
97
|
+
they did provide a
|
98
|
+
[list of notifications](http://developer.apple.com/library/mac/#documentation/UserExperience/Reference/Accessibility_RoleAttribute_Ref/Notifications.html#//apple_ref/doc/uid/TP40007870-Notifications-SW1)
|
99
|
+
which has been nicely updated for Mac OS X Lion. The list of
|
100
|
+
notifications is __required__ reading if you want to use accessibility
|
101
|
+
notifications.
|
102
|
+
|
103
|
+
For custom notifications, which I'll get to in a bit, you shouldn't
|
104
|
+
(read: don't) do a name translation unless you provide an equivalent
|
105
|
+
constant for the MacRuby run time. Instead, you should just provide
|
106
|
+
the string with the notification name:
|
107
|
+
|
108
|
+
register_for_notification app, 'customNotification'
|
109
|
+
register_for_notification app, 'Cheezburger''
|
110
|
+
|
111
|
+
### Do Something
|
112
|
+
|
113
|
+
To trigger the action, you perform an action. This was already covered
|
114
|
+
in the {file:docs/Acting.markdown Acting tutorial}, which is probably
|
115
|
+
why you are reading this tutorial.
|
116
|
+
|
117
|
+
### Waiting...
|
118
|
+
|
119
|
+
Once the action has been triggered, you simply need to wait for the
|
120
|
+
notification to be received:
|
121
|
+
|
122
|
+
wait_for_notification
|
123
|
+
|
124
|
+
And then you wait (if you are the computer or just enjoy watching your
|
125
|
+
scripts execute). If all goes well you will receive the notification,
|
126
|
+
and the script continues on to the next statement. By default,
|
127
|
+
AXElements will wait for 10 seconds, and if the notification is not
|
128
|
+
received then the script will continue anyways.
|
129
|
+
|
130
|
+
Waiting for longer, or less, time than the default can be done by
|
131
|
+
passing a parameter to `wait_for_notification`. For instance, you
|
132
|
+
could wait for a minute or a second:
|
133
|
+
|
134
|
+
wait_for_notification 60.0
|
135
|
+
wait_for_notification 1.0
|
136
|
+
|
137
|
+
## A Bit More Complicated
|
138
|
+
|
139
|
+
The above should cover most cases, but notifications can be more
|
140
|
+
complex than those examples. You might need to unregister for
|
141
|
+
notifications, or add more complex logic to a notification
|
142
|
+
registration.
|
143
|
+
|
144
|
+
### Unregister Notifications
|
145
|
+
|
146
|
+
If you need to unregister for a notifications then you can. The DSL
|
147
|
+
layer provides {Accessibility::Language#unregister_notifications}, and
|
148
|
+
hopefully more options in the future.
|
149
|
+
|
150
|
+
Using `unregister_notifications`, you can unregister _all_
|
151
|
+
notifications. This is actually done in cases where a notification is
|
152
|
+
not received, simply to clear out possible problems with lingering
|
153
|
+
registrations that might mess up future waiting.
|
154
|
+
|
155
|
+
### You Are The Decider
|
156
|
+
|
157
|
+
When you setup a notification you can optionally pass a block that
|
158
|
+
decides whether or not the received notification is the one you
|
159
|
+
wanted. You will be given the element that sent the notification,
|
160
|
+
which is not necessarily the element that you registered with, and the
|
161
|
+
name of the notification. In this case you would register for a
|
162
|
+
notification like so:
|
163
|
+
|
164
|
+
register_for_notification app, :window_created do |window, notification|
|
165
|
+
window.title == 'New Contact'
|
166
|
+
end
|
167
|
+
|
168
|
+
This gives you some extra security. Instead of continuing the script
|
169
|
+
execution after getting the first `:window_created` notification, the
|
170
|
+
script will remain paused until the element that sends the
|
171
|
+
notification has a title of `'New Contact'`.
|
172
|
+
|
173
|
+
The contract is quite simple. If you do not pass a block, then the
|
174
|
+
first notification received will cause AXElements to stop waiting; if
|
175
|
+
you want to pass a block then the block must return tuthy or falsy
|
176
|
+
to decide whether the received notification is the one that you
|
177
|
+
wanted. You will be given the sender of the notification as well as
|
178
|
+
the notification name; though the notification name will usually not
|
179
|
+
be very interesting since it is the value you registered with.
|
180
|
+
|
181
|
+
### More Than One Notification
|
182
|
+
|
183
|
+
You can register for as many notifications as you want, but I don't
|
184
|
+
recommend that you register for more than one at a time unless you
|
185
|
+
know what you are doing. The reason why is that when you call
|
186
|
+
`wait_for_notification`, you are pausing script execution until any of
|
187
|
+
the notifications you have registered for are received. The block that
|
188
|
+
you pass will only be used for notifications that are sent to the
|
189
|
+
registration you set it up with.
|
190
|
+
|
191
|
+
This is why AXElements unregisters everything if it fails to receive a
|
192
|
+
notification. This detail might also screw you over if you are trying
|
193
|
+
to work with more than one notification at once and should be fixed in
|
194
|
+
the future.
|
195
|
+
|
196
|
+
### Rainbows And Unicorns
|
197
|
+
|
198
|
+
As mentioned earlier, accessibility notifications are not all rainbows
|
199
|
+
and unicorns. If you've played with notifications while going through
|
200
|
+
this tutorial then you may have noticed a problem or two with the
|
201
|
+
design.
|
202
|
+
|
203
|
+
#### Who Sends The Notification
|
204
|
+
|
205
|
+
Documentation is the only hint you really get about who will send
|
206
|
+
particular notifications. If you are confident that a certain
|
207
|
+
notification will be sent but do not know who will be sending the
|
208
|
+
notification, then you can register with the {AX::Application} object
|
209
|
+
to be the sender of the notification. This causes you to receive
|
210
|
+
notifications from any object as long as it has the proper
|
211
|
+
notification type. If you use this feature in conjunction with passing
|
212
|
+
a block that always returns false, then you can capture all the
|
213
|
+
notifications for a period of time and find out who is sending the
|
214
|
+
notification. An example would look something like this:
|
215
|
+
|
216
|
+
register_for_notification app, :value_changed do |element, notif|
|
217
|
+
puts element.inspect
|
218
|
+
false
|
219
|
+
end
|
220
|
+
|
221
|
+
# trigger some action
|
222
|
+
|
223
|
+
wait_for_notification 30.0
|
224
|
+
|
225
|
+
Since you are returning `false` at the end of the block, the script
|
226
|
+
will pause until a timeout occurs and you will see the `#inspect`
|
227
|
+
output for each object that sends the notification.
|
228
|
+
|
229
|
+
### What Is Known To Work
|
230
|
+
|
231
|
+
- `:value_changed` is only sent by objects that have a `value`
|
232
|
+
attribute, and should be sent every time that the value is changed,
|
233
|
+
whether through accessibility or normal computer usage
|
234
|
+
- `:menu_opened` seems to only be sent by menu bar menus, not by menus
|
235
|
+
opened by pop up buttons
|
236
|
+
|
237
|
+
## Custom Notifications
|
238
|
+
|
239
|
+
Custom notifications are pretty simple. Apple recommends that you try
|
240
|
+
to use the built in notifications as much as possible, but
|
241
|
+
acknowledges that you may need to add your own notifications
|
242
|
+
sometimes.
|
243
|
+
|
244
|
+
As noted earlier, accessibility notifications use a different system
|
245
|
+
than cocoa notifications; accessibility notifications are much simpler
|
246
|
+
to send; it is simply a C function call with two parameters:
|
247
|
+
|
248
|
+
NSAccessibilityPostNotification(self, @"Cheezburger");
|
249
|
+
|
250
|
+
The first parameter is the sender of the notification; just as with
|
251
|
+
other accessibility APIs from the server (read: app) side of things,
|
252
|
+
you pass the actual object and accessibility magic will create the
|
253
|
+
token to send to the client (read: AXElements). The one caveat in this
|
254
|
+
case is that the sender of the notification cannot be an object that is
|
255
|
+
ignored by accessibility or else the notification will not be sent. If
|
256
|
+
you are unsure whether or not the object is visible to accessibility,
|
257
|
+
then you need to call an extra function to find someone who _will_ be
|
258
|
+
visible to accessibility; these functions are listed in the
|
259
|
+
[AppKit Functions](http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Miscellaneous/AppKit_Functions/Reference/reference.html#//apple_ref/doc/uid/TP40004154)
|
260
|
+
in the Accessibility section. An example would look like this:
|
261
|
+
|
262
|
+
NSAccessibilityPostNotification(NSAccessibilityUnignoredAncestor(self), @"Cheezburger");
|
263
|
+
|
264
|
+
In this case you would be looking up the hierarchy for an ancestor,
|
265
|
+
but you could also look down the hierarchy for a descendant or to the
|
266
|
+
side for a sibling.
|
267
|
+
|
268
|
+
As a rule of thumb, if you end up spending more 20minutes trying to
|
269
|
+
figure out which notification will should be listening for, and who
|
270
|
+
will be sending it, then you should just create a custom
|
271
|
+
notification. But you don't have to take my word for it.
|
@@ -0,0 +1,250 @@
|
|
1
|
+
# Searching
|
2
|
+
|
3
|
+
Searching the view hierarchy is the most powerful idea that this
|
4
|
+
toolkit provides by _significantly_ simplifying the process of
|
5
|
+
describing UI elements. Search works by looking at the child elements
|
6
|
+
of the current element, and possibly at the grandchildren, and so on
|
7
|
+
and so forth in a breadth first searching order. There are a few well
|
8
|
+
defined features of searching that make it powerful: pluralization,
|
9
|
+
attribute filtering, nesting, and element attribute inference.
|
10
|
+
|
11
|
+
## Search Basics
|
12
|
+
|
13
|
+
First, the basic form of a search looks like this:
|
14
|
+
|
15
|
+
$ELEMENT.$KLASS($FILTER_ATTRIBUTE1: $FILTER_VALUE1, ...)
|
16
|
+
|
17
|
+
Actually, that is just the form for an implicit search, which is a
|
18
|
+
little nicer to write than an explicit search. The only difference
|
19
|
+
between the two is that an implicit search must always find something,
|
20
|
+
since we are treating it as a description, or else a
|
21
|
+
{AX::Element::SearchFailure} error will be raised.
|
22
|
+
|
23
|
+
Looking at the pieces of the statement, the obvious piece is
|
24
|
+
`$ELEMENT`; `$ELEMENT` is the element to start searching from. Then
|
25
|
+
`$KLASS`, the method name, is the class of the object to search for,
|
26
|
+
and the parameter list is just a hash where the key,
|
27
|
+
`$FILTER_ATTRIBUTE`, is an attribute on the instance of the class and
|
28
|
+
the value, `$FILTER_VALUE`, needs to match what is returned when the
|
29
|
+
attribute is looked up.
|
30
|
+
|
31
|
+
An implicit search is written as if the class of the object you were
|
32
|
+
searching for was the method and the filters would be the parameters
|
33
|
+
for the method. If we substitute real values in, an example would like
|
34
|
+
this:
|
35
|
+
|
36
|
+
window.button(title: 'Main Window')
|
37
|
+
|
38
|
+
Which means that we want to find a `button` that is a child (or
|
39
|
+
descendant) of `window`, but only if the `button` has a `title` of
|
40
|
+
`'Main Window'` on it. You can add as many filters as you want, but
|
41
|
+
generally you will only need one.
|
42
|
+
|
43
|
+
### Pluralization
|
44
|
+
|
45
|
+
In cases where you want to find more than one object of a certain
|
46
|
+
type, you simply need to pluralize the method name. For example:
|
47
|
+
|
48
|
+
window.buttons
|
49
|
+
|
50
|
+
is translated into something like this:
|
51
|
+
|
52
|
+

|
53
|
+
|
54
|
+
It's just that easy. The rules for pluralization are the same as
|
55
|
+
English (or the local language?) since we are using the
|
56
|
+
`ActiveSupport::Inflector` to do the work of translating from
|
57
|
+
pluralized form back to the singular form. Even something like 'boxes'
|
58
|
+
will get translated back to 'box' and work as you expect.
|
59
|
+
|
60
|
+
Except for the fact that you will get a collection of UI elements back
|
61
|
+
instead of a single item, pluralized search works the same as a
|
62
|
+
non-pluralized search. You can attach any filters you could use with a
|
63
|
+
non-pluralized search, and if the search is implicit then it must find
|
64
|
+
something.
|
65
|
+
|
66
|
+
Pluralized searches are useful when you want to do some custom
|
67
|
+
refinement on a search, or if you need to make sure something is not
|
68
|
+
in the UI element tree (and hopefully that means it is not on the
|
69
|
+
screen anymore either). It can also be helpful when you want to
|
70
|
+
explore the UI element tree and find out what types of UI elements are
|
71
|
+
present on the screen. The detail to remember is that a pluralized
|
72
|
+
search will have to explore the entire UI sub-tree from the starting
|
73
|
+
point and so it could be slow.
|
74
|
+
|
75
|
+
### Kind Of
|
76
|
+
|
77
|
+
Remember earlier when I said the method name should be the class of
|
78
|
+
the element being searched for? Well, that was kind of a lie. Kind of.
|
79
|
+
Get it? Have I killed the joke yet? Kind of? :D
|
80
|
+
|
81
|
+
The search class that you enter is actually matched using `#kind_of?`
|
82
|
+
instead of matching the classes exactly. And since class hierarchies
|
83
|
+
are properly setup, you can search for a base class and end up finding
|
84
|
+
subclasses.
|
85
|
+
|
86
|
+
For instance, not all buttons are of the class `AX::Button`, the
|
87
|
+
traffic light buttons are all different subclasses of `AX::Button`:
|
88
|
+
`AX::CloseButton`, `AX::MinimizeButton`, and `AX::ZoomButton`. There
|
89
|
+
are other several other subclasses of `AX::Button` as
|
90
|
+
well. `AX::Button` itself is a subclass of `AX::Element`. Actually,
|
91
|
+
all UI elements are a subclass of `AX::Element`. What this means is
|
92
|
+
that when you have code like:
|
93
|
+
|
94
|
+
app.close_button
|
95
|
+
|
96
|
+
You will only ever find something that is a `AX::CloseButton`, but
|
97
|
+
when you write something like:
|
98
|
+
|
99
|
+
app.button
|
100
|
+
|
101
|
+
Any button or subclass of button, including all the traffic light
|
102
|
+
buttons, can be found. I believe this makes search follow the
|
103
|
+
[DWIM](http://en.wikipedia.org/wiki/DWIM) principle, and allows you to
|
104
|
+
shorten the code you need to write in a number of cases. For example:
|
105
|
+
|
106
|
+
app.window
|
107
|
+
|
108
|
+
Can be substituted in place of
|
109
|
+
|
110
|
+
app.standard_window
|
111
|
+
|
112
|
+
to find the first window for `app`. This makes sense if there is only
|
113
|
+
one window for the app, which is often the case. Similarly, if you are
|
114
|
+
searching from a container, such as an `AX::Group`, which only has a
|
115
|
+
one button, which happens to be a `AX::SortButton`, then you can say:
|
116
|
+
|
117
|
+
table.button
|
118
|
+
|
119
|
+
Since it will not be ambiguous and AXElements knows what you
|
120
|
+
mean. What if we take it a step further, what if made an even broader
|
121
|
+
search. Since _all_ UI elements are a subclass of {AX::Element}, we
|
122
|
+
could just write something like:
|
123
|
+
|
124
|
+
app.element
|
125
|
+
|
126
|
+
Which would find the first child of `app`. If we combined this with
|
127
|
+
pluralization, we could do something like:
|
128
|
+
|
129
|
+
app.elements
|
130
|
+
|
131
|
+
Which will return an array with _all_ the descendants in it; so as always
|
132
|
+
you will need to have some awareness of the layout of the element tree
|
133
|
+
when you write a search. Otherwise you could end up finding something
|
134
|
+
completely different from what you wanted; consider what could happen
|
135
|
+
if you search for an `AX::Button` objects when you want an
|
136
|
+
`AX::CloseButton`. In these cases you will want to be more specific
|
137
|
+
about what you are looking for, which can often allow you to skip the
|
138
|
+
need for a search filter. Be specific when it sounds better and
|
139
|
+
generalize more when you can, it should make code read more
|
140
|
+
naturally.
|
141
|
+
|
142
|
+
### Nested Searching
|
143
|
+
|
144
|
+
Sometimes you need to describe an element on the screen, and the only
|
145
|
+
reasonable way to do so is by describing some of the descendants of the
|
146
|
+
element that you are looking for. For this requirement, nested
|
147
|
+
searching exists, and very naturally too. Pretend that you didn't
|
148
|
+
already look at the next code snippet and try to guess what a nested
|
149
|
+
search looks like; you will probably be correct. __Hint__: Searches
|
150
|
+
can be nested arbitrarily deep and mixed in with other filter
|
151
|
+
parameters. The answer, by example:
|
152
|
+
|
153
|
+
window.outline.row(text_field: { value: 'Calendar' })
|
154
|
+
|
155
|
+
This would be asking for the outline row that has a text field with
|
156
|
+
the value of `'Calendar'`. The proper form for this would be:
|
157
|
+
|
158
|
+
$ELEMENT.$KLASS($DESCENDANT: { $DESCENDANT_FILTER_ATTRIBUTE: $DESCENDANT_FILTER_VALUE1, ... }, ...)
|
159
|
+
|
160
|
+
Where `$DESCENDANT` plays the same role as `$KLASS`, but for a new
|
161
|
+
search that will be applied to descendants of `$KLASS`. Nested
|
162
|
+
searching is a feature you won't need too often if the UI hierarchy
|
163
|
+
makes good use of identifiers or other attributes that can be used to
|
164
|
+
uniquely identify an element. Nested searching is best used when you
|
165
|
+
can only identify an element by describing its children. But you don't
|
166
|
+
have to take my word for it.
|
167
|
+
|
168
|
+
### Element Attribute Inference
|
169
|
+
|
170
|
+
Element attribute inference came about because of a coding error, when
|
171
|
+
someone was trying to write some code to search for element that
|
172
|
+
matched to a title UI element. Before going over the solution I think
|
173
|
+
it would be best to explain the problem.
|
174
|
+
|
175
|
+
If an element has a title UI element attribute, then the title UI
|
176
|
+
element will end up being another UI element. The problem with this is
|
177
|
+
that you then need to know about that element before you search and
|
178
|
+
then you need to use the element as the filter value, for example:
|
179
|
+
|
180
|
+
title_field = window.text_field(value: 'Name')
|
181
|
+
window.button(title_ui_element: title_field)
|
182
|
+
|
183
|
+
While that code is legitimate, it is not the most succinct way of
|
184
|
+
writing what was meant, and maybe not as clear as it could be. Perhaps
|
185
|
+
something more like:
|
186
|
+
|
187
|
+
window.button(title_ui_element: 'Name')
|
188
|
+
|
189
|
+
In this case you are matching the `title` of the
|
190
|
+
`title_ui_element`. This works without introducing inconsistencies in
|
191
|
+
the language we have created for searching. In the example, you would
|
192
|
+
be saying that the button you are looking for will be associated with
|
193
|
+
a title UI element that says `'Name'`. You can still match against the
|
194
|
+
actual UI element if you want, but I think this is much simpler.
|
195
|
+
|
196
|
+
This is not actually implemented internally to the searching
|
197
|
+
logic, it is implemented in each class that wants to
|
198
|
+
participate. Since search works by getting the value of the search
|
199
|
+
filter and checking if it is `==` to the filter value, we just need
|
200
|
+
to implement `==` on specific classes where we want to support custom
|
201
|
+
behaviour. Examples of how this would work would be
|
202
|
+
{AX::StaticText#==} and {AX::Button#==}. The behaviour could be easily
|
203
|
+
added to other classes where it made sense. However, this type of idea
|
204
|
+
can be further expanded to cases that might not be so easily
|
205
|
+
implemented and is discussed in future prospects.
|
206
|
+
|
207
|
+
## Explicit Search
|
208
|
+
|
209
|
+
In the off chance that you need to make an explicit search, you can
|
210
|
+
trigger a search through {AX::Element#search}. In this case you give
|
211
|
+
the `$KLASS` as the first parameter of the method and the filters are
|
212
|
+
the remaining parameters. As with {AX::Element#attribute}, this is
|
213
|
+
meant for performance in cases of heavy searching; you should avoid
|
214
|
+
using it unless you know what you are doing.
|
215
|
+
|
216
|
+
## Caveats
|
217
|
+
|
218
|
+
The only caveat to note right now is that a UI element will always
|
219
|
+
return `false` when you ask if it can `#respond_to?` something that
|
220
|
+
would be an implicit search. This is because of the semantics of a
|
221
|
+
search do not make sense in the context of `#respond_to?` and would be
|
222
|
+
very expensive. You would need to perform the search in order to know
|
223
|
+
if the search would succeed.
|
224
|
+
|
225
|
+
## The Future
|
226
|
+
|
227
|
+
Right now, the only case that is not handled is filtering by
|
228
|
+
parameterized attribute. The problem with this case is that I am not
|
229
|
+
sure how to work it into the existing syntax or how to change the
|
230
|
+
existing syntax. Since you also need to encode the parameter with the
|
231
|
+
attribute it is difficult to express in terms of key-value
|
232
|
+
pairs. Perhaps the key could be an array so that the attribute can be
|
233
|
+
included with the key? That would be possible without too much work,
|
234
|
+
but how would it look? Does the syntax for search start to get too
|
235
|
+
crazy at that point? For instance, you cannot use the label syntax for
|
236
|
+
hash keys with an array (unfortunately), so code would look like:
|
237
|
+
|
238
|
+
window.button([:string_for_range, CFRange.new(0,5)] => 'AXEle')
|
239
|
+
|
240
|
+
This topic is open to debate, but I will always play the part of the
|
241
|
+
devil's advocate. :)
|
242
|
+
|
243
|
+
### Inference
|
244
|
+
|
245
|
+
There is also space for enhancement in the attribute inference
|
246
|
+
feature(s). Wouldn't it be nice to be able to specify a filter value
|
247
|
+
as a range instead of a specific value? Probably not very often, but it is
|
248
|
+
something that could be done. Search filtering is not very flexible in
|
249
|
+
that regard right now, and maybe it never needs to be, but it is an
|
250
|
+
interesting change to think about.
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Test Helpers
|
2
|
+
|
3
|
+
@todo Pretend that this does not exist yet
|
4
|
+
|
5
|
+
There are some types of assertions that you would like to make during
|
6
|
+
testing that simply does not make sense using the build in
|
7
|
+
assertions. These can include things like existence checks which you
|
8
|
+
would have to implement by searching, or ... that is really the only
|
9
|
+
one I have so far :)
|
10
|
+
|
11
|
+
## RSpec
|
12
|
+
|
13
|
+
RSpec 2 is the Marketcircle choice for implementing functional and
|
14
|
+
behavioural tests for apps using AXElements. It is great and offers
|
15
|
+
the flexibility and features that make using it very good for large
|
16
|
+
test suites.
|
17
|
+
|
18
|
+
You can load the RSpec matchers that AXElements adds by requiring
|
19
|
+
|
20
|
+
require 'rspec/ax_elements'
|
21
|
+
|
22
|
+
### Existence
|
23
|
+
|
24
|
+
Checking for the existence of an object in RSpec with AXElements looks
|
25
|
+
a bit awkward:
|
26
|
+
|
27
|
+
window.search(:button).should be_empty
|
28
|
+
# or
|
29
|
+
window.search(:button).should_not be_empty
|
30
|
+
|
31
|
+
That does not communicate intent as clearly as it could. What if you
|
32
|
+
could say something like:
|
33
|
+
|
34
|
+
window.should have_a :button
|
35
|
+
# or
|
36
|
+
window.should_not have_a :button
|
37
|
+
|
38
|
+
What if you have filters? Well, that is handled as well, though maybe
|
39
|
+
not as nicely:
|
40
|
+
|
41
|
+
window.should have_a(:button).with(title: 'Hello')
|
42
|
+
|
43
|
+
## Minitest
|
44
|
+
|
45
|
+
AXElements uses minitest for its own regression test suite. Minitest
|
46
|
+
is pretty cool, and there is good cause to support it as well. To that
|
47
|
+
end, we have provided the equivalent assertions.
|
48
|
+
|
49
|
+
You can load it like so:
|
50
|
+
|
51
|
+
require 'minitest/ax_elements'
|
52
|
+
|
data/docs/images/AX.png
ADDED
Binary file
|
Binary file
|
@@ -0,0 +1,34 @@
|
|
1
|
+
digraph {
|
2
|
+
Application [label = "Application: Mail", style=filled]
|
3
|
+
MainWindow [label = "MainWindow: Inbox"]
|
4
|
+
Outline [label = "Outline"]
|
5
|
+
Table [label = "Table: Emails"]
|
6
|
+
TableRow1 [label = "TableRow: Spam"]
|
7
|
+
TableRow2 [label = "TableRow: Daily LOLcat"]
|
8
|
+
MenuBar [label = "MenuBar", style=filled]
|
9
|
+
MenuBarItem1 [label = "MenuBarItem: File", style=filled]
|
10
|
+
MenuBarItem2 [label = "MenuBarItem: Edit"]
|
11
|
+
MenuBarItem3 [label = "MenuBarItem: Help"]
|
12
|
+
Menu1 [label = "Menu", style=filled]
|
13
|
+
MenuItem1 [label = "MenuItem: Preferences"]
|
14
|
+
MenuItem2 [label = "MenuItem: Services", style=filled]
|
15
|
+
MenuItem3 [label = "MenuItem: Quit"]
|
16
|
+
Menu2 [label = "Menu", style=filled]
|
17
|
+
MenuItem4 [label = "MenuItem: Services Preferences", style=filled]
|
18
|
+
|
19
|
+
Application -> MainWindow
|
20
|
+
Application -> MenuBar
|
21
|
+
MainWindow -> Outline
|
22
|
+
MainWindow -> Table
|
23
|
+
Table -> TableRow1
|
24
|
+
Table -> TableRow2
|
25
|
+
MenuBar -> MenuBarItem1
|
26
|
+
MenuBar -> MenuBarItem2
|
27
|
+
MenuBar -> MenuBarItem3
|
28
|
+
MenuBarItem1 -> Menu1
|
29
|
+
Menu1 -> MenuItem1
|
30
|
+
Menu1 -> MenuItem2
|
31
|
+
Menu1 -> MenuItem3
|
32
|
+
MenuItem2 -> Menu2
|
33
|
+
Menu2 -> MenuItem4
|
34
|
+
}
|
Binary file
|