AXElements 0.6.0beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![All The Buttons](images/all_the_buttons.jpg)
|
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
|