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.
Files changed (54) hide show
  1. data/.yardopts +20 -0
  2. data/LICENSE.txt +25 -0
  3. data/README.markdown +150 -0
  4. data/Rakefile +109 -0
  5. data/docs/AccessibilityTips.markdown +119 -0
  6. data/docs/Acting.markdown +340 -0
  7. data/docs/Debugging.markdown +326 -0
  8. data/docs/Inspecting.markdown +255 -0
  9. data/docs/KeyboardEvents.markdown +57 -0
  10. data/docs/NewBehaviour.markdown +151 -0
  11. data/docs/Notifications.markdown +271 -0
  12. data/docs/Searching.markdown +250 -0
  13. data/docs/TestingExtensions.markdown +52 -0
  14. data/docs/images/AX.png +0 -0
  15. data/docs/images/all_the_buttons.jpg +0 -0
  16. data/docs/images/ui_hierarchy.dot +34 -0
  17. data/docs/images/ui_hierarchy.png +0 -0
  18. data/ext/key_coder/extconf.rb +6 -0
  19. data/ext/key_coder/key_coder.m +77 -0
  20. data/lib/ax_elements/accessibility/enumerators.rb +104 -0
  21. data/lib/ax_elements/accessibility/language.rb +347 -0
  22. data/lib/ax_elements/accessibility/qualifier.rb +73 -0
  23. data/lib/ax_elements/accessibility.rb +164 -0
  24. data/lib/ax_elements/core.rb +541 -0
  25. data/lib/ax_elements/element.rb +593 -0
  26. data/lib/ax_elements/elements/application.rb +88 -0
  27. data/lib/ax_elements/elements/button.rb +18 -0
  28. data/lib/ax_elements/elements/radio_button.rb +18 -0
  29. data/lib/ax_elements/elements/row.rb +30 -0
  30. data/lib/ax_elements/elements/static_text.rb +17 -0
  31. data/lib/ax_elements/elements/systemwide.rb +46 -0
  32. data/lib/ax_elements/inspector.rb +116 -0
  33. data/lib/ax_elements/macruby_extensions.rb +255 -0
  34. data/lib/ax_elements/notification.rb +37 -0
  35. data/lib/ax_elements/version.rb +9 -0
  36. data/lib/ax_elements.rb +30 -0
  37. data/lib/minitest/ax_elements.rb +19 -0
  38. data/lib/mouse.rb +185 -0
  39. data/lib/rspec/expectations/ax_elements.rb +15 -0
  40. data/test/elements/test_application.rb +72 -0
  41. data/test/elements/test_row.rb +27 -0
  42. data/test/elements/test_systemwide.rb +38 -0
  43. data/test/helper.rb +119 -0
  44. data/test/test_accessibility.rb +127 -0
  45. data/test/test_blankness.rb +26 -0
  46. data/test/test_core.rb +448 -0
  47. data/test/test_element.rb +939 -0
  48. data/test/test_enumerators.rb +81 -0
  49. data/test/test_inspector.rb +121 -0
  50. data/test/test_language.rb +157 -0
  51. data/test/test_macruby_extensions.rb +303 -0
  52. data/test/test_mouse.rb +5 -0
  53. data/test/test_search_semantics.rb +143 -0
  54. 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