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
data/test/test_core.rb
ADDED
@@ -0,0 +1,448 @@
|
|
1
|
+
class TestCore < TestAX
|
2
|
+
|
3
|
+
WINDOW = attribute_for REF, KAXMainWindowAttribute
|
4
|
+
|
5
|
+
def child name
|
6
|
+
children_for(WINDOW).find do |item|
|
7
|
+
attribute_for(item, KAXRoleAttribute) == name
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def slider
|
12
|
+
@@slider ||= child KAXSliderRole
|
13
|
+
end
|
14
|
+
|
15
|
+
def check_box
|
16
|
+
@@check_box ||= child KAXCheckBoxRole
|
17
|
+
end
|
18
|
+
|
19
|
+
def search_box
|
20
|
+
@@search_box ||= child KAXTextFieldRole
|
21
|
+
end
|
22
|
+
|
23
|
+
def button
|
24
|
+
@@button ||= child KAXButtonRole
|
25
|
+
end
|
26
|
+
|
27
|
+
def static_text
|
28
|
+
@@static_text ||= child KAXStaticTextRole
|
29
|
+
end
|
30
|
+
|
31
|
+
def yes_button
|
32
|
+
@@yes_button ||= children_for(WINDOW).find do |item|
|
33
|
+
if attribute_for(item, KAXRoleAttribute) == KAXButtonRole
|
34
|
+
attribute_for(item, KAXTitleAttribute) == 'Yes'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
##
|
43
|
+
# AFAICT every accessibility object **MUST** have attributes, so
|
44
|
+
# there are no tests to check what happens when they do not exist;
|
45
|
+
# though I am quite sure that AXElements will explode.
|
46
|
+
class TestAttrsOfElement < TestCore
|
47
|
+
|
48
|
+
def attrs
|
49
|
+
@@attrs ||= AX.attrs_of_element REF
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_returns_array_of_strings
|
53
|
+
assert_instance_of String, attrs.first
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_make_sure_certain_attributes_are_provided
|
57
|
+
assert_includes attrs, KAXRoleAttribute
|
58
|
+
assert_includes attrs, KAXRoleDescriptionAttribute
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_other_attributes_that_the_app_should_have
|
62
|
+
assert_includes attrs, KAXChildrenAttribute
|
63
|
+
assert_includes attrs, KAXTitleAttribute
|
64
|
+
assert_includes attrs, KAXMenuBarAttribute
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
class TestAttrCountOfElement < TestCore
|
71
|
+
|
72
|
+
def test_returns_number_of_children
|
73
|
+
assert_equal children_for(REF).size, AX.attr_count_of_element(REF, KAXChildrenAttribute)
|
74
|
+
assert_equal 0, AX.attr_count_of_element(button, KAXChildrenAttribute)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @todo there are things we care about?
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
class TestAttrOfElementGetsCorrectAttribute < TestCore
|
83
|
+
|
84
|
+
def test_title_is_title
|
85
|
+
assert_equal 'AXElementsTester', AX.attr_of_element(REF, KAXTitleAttribute)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @note the app gives CGRectZero in screen coordinates, and then they are
|
89
|
+
# flipped for us to, so we need to flip them again
|
90
|
+
def test_custom_lol_is_rect
|
91
|
+
point = CGPointZero.dup
|
92
|
+
point.y = NSScreen.mainScreen.frame.size.height
|
93
|
+
expected_rect = CGRect.new point, CGSizeZero
|
94
|
+
ret = AX.attr_of_element WINDOW, 'AXLol'
|
95
|
+
ptr = Pointer.new CGRect.type
|
96
|
+
AXValueGetValue(ret, 3, ptr)
|
97
|
+
assert_equal expected_rect, ptr[0]
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_hidden_is_hidden_value
|
101
|
+
assert_equal false, AX.attr_of_element(REF, KAXHiddenAttribute)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
class TestAttrOfElementErrors < TestCore
|
108
|
+
include LoggingCapture
|
109
|
+
|
110
|
+
def test_logs_message_for_non_existant_attributes
|
111
|
+
with_logging do AX.attr_of_element REF, 'MADEUPATTRIBUTE' end
|
112
|
+
assert_match /#{KAXErrorAttributeUnsupported}/, @log_output.string
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
class TestAttrOfElementWritable < TestCore
|
119
|
+
|
120
|
+
def test_true_for_writable_attribute
|
121
|
+
assert AX.attr_of_element_writable?(WINDOW, KAXMainAttribute)
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_false_for_non_writable_attribute
|
125
|
+
refute AX.attr_of_element_writable?(REF, KAXTitleAttribute)
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_false_for_non_existante_attribute
|
129
|
+
refute AX.attr_of_element_writable?(REF, 'FAKE')
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
class TestSetAttrOfElement < TestCore
|
136
|
+
|
137
|
+
def test_set_a_slider
|
138
|
+
[25, 75, 50].each do |value|
|
139
|
+
AX.set_attr_of_element slider, KAXValueAttribute, value
|
140
|
+
assert_equal value, value_for(slider)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_set_a_text_field
|
145
|
+
[Time.now.to_s, ''].each do |value|
|
146
|
+
AX.set_attr_of_element(search_box, KAXValueAttribute, value)
|
147
|
+
assert_equal value, value_for(search_box)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
class TestActionsOfElement < TestCore
|
155
|
+
|
156
|
+
def test_works_when_there_are_no_actions
|
157
|
+
assert_empty AX.actions_of_element REF
|
158
|
+
end
|
159
|
+
|
160
|
+
def test_returns_array_of_strings
|
161
|
+
assert_instance_of String, AX.actions_of_element(yes_button).first
|
162
|
+
end
|
163
|
+
|
164
|
+
def test_make_sure_certain_actions_are_present
|
165
|
+
actions = AX.actions_of_element slider
|
166
|
+
assert_includes actions, KAXIncrementAction
|
167
|
+
assert_includes actions, KAXDecrementAction
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
class TestActionOfElement < TestCore
|
174
|
+
|
175
|
+
def test_check_a_check_box
|
176
|
+
2.times do # twice so it should be back where it started
|
177
|
+
value = value_for check_box
|
178
|
+
AX.action_of_element check_box, KAXPressAction
|
179
|
+
refute_equal value, value_for(check_box)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_sliding_the_slider
|
184
|
+
value = attribute_for slider, KAXValueAttribute
|
185
|
+
AX.action_of_element slider, KAXIncrementAction
|
186
|
+
assert attribute_for(slider, KAXValueAttribute) > value
|
187
|
+
|
188
|
+
value = attribute_for slider, KAXValueAttribute
|
189
|
+
AX.action_of_element slider, KAXDecrementAction
|
190
|
+
assert attribute_for(slider, KAXValueAttribute) < value
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
class TestKeyboardAction < TestCore
|
197
|
+
|
198
|
+
SYSTEM = AXUIElementCreateSystemWide()
|
199
|
+
|
200
|
+
def post string
|
201
|
+
set_attribute_for search_box, KAXFocusedAttribute, true
|
202
|
+
AX.keyboard_action REF, string
|
203
|
+
# sleep 0.01
|
204
|
+
assert_equal string, attribute_for(search_box, KAXValueAttribute)
|
205
|
+
ensure
|
206
|
+
button = children_for(search_box).find { |x| x.class == AX::Button }
|
207
|
+
action_for button, KAXPressAction
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_uppercase_letters
|
211
|
+
post 'HELLO, WORLD'
|
212
|
+
end
|
213
|
+
|
214
|
+
def test_numbers
|
215
|
+
post '42'
|
216
|
+
end
|
217
|
+
|
218
|
+
def test_letters
|
219
|
+
post 'the cake is a lie'
|
220
|
+
end
|
221
|
+
|
222
|
+
def test_escape_sequences
|
223
|
+
post "\s"
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
class TestAXParamAttrsOfElement < TestCore
|
230
|
+
|
231
|
+
def test_empty_for_dock
|
232
|
+
assert_empty AX.param_attrs_of_element REF
|
233
|
+
end
|
234
|
+
|
235
|
+
def test_not_empty_for_search_field
|
236
|
+
assert_includes AX.param_attrs_of_element(static_text), KAXStringForRangeParameterizedAttribute
|
237
|
+
assert_includes AX.param_attrs_of_element(static_text), KAXLineForIndexParameterizedAttribute
|
238
|
+
assert_includes AX.param_attrs_of_element(static_text), KAXBoundsForRangeParameterizedAttribute
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
class TestAXParamAttrOfElement < TestCore
|
245
|
+
|
246
|
+
def test_contains_proper_info
|
247
|
+
attr = AX.param_attr_of_element(static_text,
|
248
|
+
KAXStringForRangeParameterizedAttribute,
|
249
|
+
CFRange.new(0, 5).to_axvalue)
|
250
|
+
assert_equal 'AXEle', attr
|
251
|
+
end
|
252
|
+
|
253
|
+
def test_get_attributed_string
|
254
|
+
attr = AX.param_attr_of_element(static_text,
|
255
|
+
# this is why we need name tranformers
|
256
|
+
KAXAttributedStringForRangeParameterizedAttribute,
|
257
|
+
CFRange.new(0, 5).to_axvalue)
|
258
|
+
assert_kind_of NSAttributedString, attr
|
259
|
+
assert_equal 'AXEle', attr.string
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
# be very careful about cleaning up state after notification tests
|
266
|
+
class TestAXNotifications < TestCore
|
267
|
+
|
268
|
+
# custom notification name
|
269
|
+
CHEEZBURGER = 'Cheezburger'
|
270
|
+
|
271
|
+
SHORT_TIMEOUT = 0.1
|
272
|
+
TIMEOUT = 1.0
|
273
|
+
|
274
|
+
def radio_group
|
275
|
+
@radio_group ||= child KAXRadioGroupRole
|
276
|
+
end
|
277
|
+
|
278
|
+
def radio_gaga
|
279
|
+
@@gaga ||= children_for(radio_group).find do |item|
|
280
|
+
attribute_for(item, KAXTitleAttribute) == 'Gaga'
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def radio_flyer
|
285
|
+
@@flyer ||= children_for(radio_group).find do |item|
|
286
|
+
attribute_for(item, KAXTitleAttribute) == 'Flyer'
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def teardown
|
291
|
+
if attribute_for(radio_gaga, KAXValueAttribute) == 1
|
292
|
+
action_for radio_flyer, KAXPressAction
|
293
|
+
end
|
294
|
+
AX.unregister_notifs
|
295
|
+
end
|
296
|
+
|
297
|
+
def test_break_if_ignoring
|
298
|
+
AX.register_for_notif(radio_gaga, KAXValueChangedNotification) { |_,_| true }
|
299
|
+
AX.instance_variable_set :@ignore_notifs, true
|
300
|
+
|
301
|
+
action_for radio_gaga, KAXPressAction
|
302
|
+
|
303
|
+
start = Time.now
|
304
|
+
refute AX.wait_for_notif(SHORT_TIMEOUT)
|
305
|
+
done = Time.now
|
306
|
+
|
307
|
+
refute_in_delta done, start, SHORT_TIMEOUT
|
308
|
+
end
|
309
|
+
|
310
|
+
def test_break_if_block_returns_falsey
|
311
|
+
AX.register_for_notif(radio_gaga, KAXValueChangedNotification) { |_,_| false }
|
312
|
+
action_for radio_gaga, KAXPressAction
|
313
|
+
|
314
|
+
start = Time.now
|
315
|
+
refute AX.wait_for_notif(SHORT_TIMEOUT)
|
316
|
+
done = Time.now
|
317
|
+
|
318
|
+
refute_in_delta done, start, SHORT_TIMEOUT
|
319
|
+
end
|
320
|
+
|
321
|
+
def test_stops_if_block_returns_truthy
|
322
|
+
AX.register_for_notif(radio_gaga, KAXValueChangedNotification) { |_,_| true }
|
323
|
+
action_for radio_gaga, KAXPressAction
|
324
|
+
assert AX.wait_for_notif(SHORT_TIMEOUT)
|
325
|
+
end
|
326
|
+
|
327
|
+
def test_returns_triple
|
328
|
+
ret = AX.register_for_notif(radio_gaga, KAXValueChangedNotification) { |_,_| true }
|
329
|
+
assert_equal AXObserverGetTypeID(), CFGetTypeID(ret[0])
|
330
|
+
assert_equal radio_gaga, ret[1]
|
331
|
+
assert_equal KAXValueChangedNotification, ret[2]
|
332
|
+
end
|
333
|
+
|
334
|
+
def test_wait_stops_waiting_when_notif_received
|
335
|
+
AX.register_for_notif(radio_gaga, KAXValueChangedNotification) { |_,_| true }
|
336
|
+
action_for radio_gaga, KAXPressAction
|
337
|
+
|
338
|
+
start = Time.now
|
339
|
+
AX.wait_for_notif(SHORT_TIMEOUT)
|
340
|
+
done = Time.now
|
341
|
+
|
342
|
+
msg = 'Might fail if your machine is under heavy load'
|
343
|
+
assert_in_delta done, start, 0.02, msg
|
344
|
+
end
|
345
|
+
|
346
|
+
def test_works_with_custom_notifs
|
347
|
+
got_callback = false
|
348
|
+
AX.register_for_notif(yes_button, CHEEZBURGER) do |_,_|
|
349
|
+
got_callback = true
|
350
|
+
end
|
351
|
+
action_for yes_button, KAXPressAction
|
352
|
+
AX.wait_for_notif(SHORT_TIMEOUT)
|
353
|
+
assert got_callback, 'did not get callback'
|
354
|
+
end
|
355
|
+
|
356
|
+
def test_unregistering_clears_notif
|
357
|
+
AX.register_for_notif(yes_button, CHEEZBURGER) { |_,_| true }
|
358
|
+
action_for yes_button, KAXPressAction
|
359
|
+
end
|
360
|
+
|
361
|
+
def test_unregistering_noops_if_not_registered
|
362
|
+
assert_block do
|
363
|
+
AX.unregister_notifs
|
364
|
+
AX.unregister_notifs
|
365
|
+
AX.unregister_notifs
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def test_unregistering_sets_ignore_to_true
|
370
|
+
AX.register_for_notif(yes_button, CHEEZBURGER) { |_,_| true }
|
371
|
+
refute AX.instance_variable_get(:@ignore_notifs)
|
372
|
+
AX.unregister_notifs
|
373
|
+
assert AX.instance_variable_get(:@ignore_notifs)
|
374
|
+
end
|
375
|
+
|
376
|
+
def test_listening_to_app_catches_everything
|
377
|
+
got_callback = false
|
378
|
+
AX.register_for_notif(REF, KAXValueChangedNotification) do |_,_|
|
379
|
+
got_callback = true
|
380
|
+
end
|
381
|
+
action_for radio_gaga, KAXPressAction
|
382
|
+
AX.wait_for_notif(TIMEOUT)
|
383
|
+
assert got_callback
|
384
|
+
end
|
385
|
+
|
386
|
+
end
|
387
|
+
|
388
|
+
|
389
|
+
class TestElementAtPosition < TestCore
|
390
|
+
|
391
|
+
def test_returns_a_button_when_i_give_the_coordinates_of_a_button
|
392
|
+
point = attribute_for button, KAXPositionAttribute
|
393
|
+
ptr = Pointer.new CGPoint.type
|
394
|
+
AXValueGetValue(point, KAXValueCGPointType, ptr)
|
395
|
+
point = ptr[0]
|
396
|
+
element = AX.element_at_point(*point.to_a)
|
397
|
+
assert_equal button, element
|
398
|
+
end
|
399
|
+
|
400
|
+
end
|
401
|
+
|
402
|
+
|
403
|
+
class TestAXPIDThings < TestCore
|
404
|
+
|
405
|
+
def test_app_for_pid_returns_raw_element
|
406
|
+
ret = AX.application_for_pid PID
|
407
|
+
role = attribute_for ret, KAXRoleAttribute
|
408
|
+
assert_equal KAXApplicationRole, role
|
409
|
+
end
|
410
|
+
|
411
|
+
def test_app_for_pid_raises_if_pid_is_zero
|
412
|
+
assert_raises ArgumentError do
|
413
|
+
AX.application_for_pid 0
|
414
|
+
end
|
415
|
+
assert_raises ArgumentError do
|
416
|
+
AX.application_for_pid -1
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def test_pid_for_app
|
421
|
+
assert_equal PID, AX.pid_of_element(REF)
|
422
|
+
end
|
423
|
+
|
424
|
+
def test_pid_for_dock_app_is_docks_pid
|
425
|
+
assert_equal PID, AX.pid_of_element(WINDOW)
|
426
|
+
end
|
427
|
+
|
428
|
+
end
|
429
|
+
|
430
|
+
|
431
|
+
# I'd prefer to not to be directly calling the log method bypassing
|
432
|
+
# the fact that it is a private method
|
433
|
+
class TestLogAXCall < TestCore
|
434
|
+
include LoggingCapture
|
435
|
+
|
436
|
+
def test_looks_up_code_properly
|
437
|
+
with_logging { AX.send(:log_error, REF, KAXErrorAPIDisabled) }
|
438
|
+
assert_match /API Disabled/, @log_output.string
|
439
|
+
with_logging { AX.send(:log_error, REF, KAXErrorNotImplemented) }
|
440
|
+
assert_match /Not Implemented/, @log_output.string
|
441
|
+
end
|
442
|
+
|
443
|
+
def test_handles_unknown_error_codes
|
444
|
+
with_logging { AX.send(:log_error, REF, 1234567) }
|
445
|
+
assert_match /UNKNOWN ERROR CODE/, @log_output.string
|
446
|
+
end
|
447
|
+
|
448
|
+
end
|