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
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
|