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/lib/mouse.rb ADDED
@@ -0,0 +1,185 @@
1
+ ##
2
+ # [Reference](http://developer.apple.com/library/mac/#documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html).
3
+ #
4
+ # @todo Inertial scrolling
5
+ # @todo Bezier paths
6
+ # @todo More intelligent default duration
7
+ # @todo Point arguments should accept a pair tuple...or should they?
8
+ # @todo Refactor to try and reuse the same event for a single action
9
+ # instead of creating new events.
10
+ # @todo Pause between down/up clicks
11
+ module Mouse; end
12
+
13
+ class << Mouse
14
+
15
+ ##
16
+ # Number of animation steps per second.
17
+ #
18
+ # @return [Number]
19
+ FPS = 120
20
+
21
+ ##
22
+ # @note We keep the number as a rational to try and avoid rounding
23
+ # error introduced by the way MacRuby deals with floats.
24
+ #
25
+ # Smallest unit of time allowed for an animation step.
26
+ #
27
+ # @return [Number]
28
+ QUANTUM = Rational(1, FPS)
29
+
30
+ ##
31
+ # Available constants for the type of units to use when scrolling.
32
+ #
33
+ # @return [Hash{Symbol=>Fixnum}]
34
+ UNIT = {
35
+ line: KCGScrollEventUnitLine,
36
+ pixel: KCGScrollEventUnitPixel
37
+ }
38
+
39
+ ##
40
+ # The coordinates of the mouse using the flipped coordinate system
41
+ # (origin in top left).
42
+ #
43
+ # @return [CGPoint]
44
+ def current_position
45
+ CGEventGetLocation(CGEventCreate(nil))
46
+ end
47
+
48
+ ##
49
+ # Move the mouse from the current position to the given point.
50
+ #
51
+ # @param [CGPoint]
52
+ # @param [Float] duration animation duration, in seconds
53
+ def move_to point, duration = 0.2
54
+ animate KCGEventMouseMoved, KCGMouseButtonLeft, current_position, point, duration
55
+ end
56
+
57
+ ##
58
+ # Click and drag from the current position to the given point.
59
+ #
60
+ # @param [CGPoint]
61
+ # @param [Float] duration animation duration, in seconds
62
+ def drag_to point, duration = 0.2
63
+ event = CGEventCreateMouseEvent(nil, KCGEventLeftMouseDown, current_position, KCGMouseButtonLeft)
64
+ CGEventPost(KCGHIDEventTap, event)
65
+ animate KCGEventLeftMouseDragged, KCGMouseButtonLeft, current_position, point, duration
66
+ event = CGEventCreateMouseEvent(nil, KCGEventLeftMouseUp, current_position, KCGMouseButtonLeft)
67
+ CGEventPost(KCGHIDEventTap, event)
68
+ end
69
+
70
+ ##
71
+ # @todo Need to double check to see if I introduce any inaccuracies.
72
+ #
73
+ # Scroll at the current position the given amount of units.
74
+ #
75
+ # Scrolling too much or too little in a period of time will cause the
76
+ # animation to look weird, possibly causing the app to mess things up.
77
+ #
78
+ # @param [Fixnum] amount number of units to scroll; positive to scroll
79
+ # up or negative to scroll down
80
+ # @param [Float] duration animation duration, in seconds
81
+ # @param [Fixnum] units `:line` scrolls by line, `:pixel` scrolls by pixel
82
+ def scroll amount, duration = 0.2, units = :line
83
+ units = UNIT[units] || raise(ArgumentError, "#{units} is not a valid unit")
84
+ steps = (FPS * duration).floor
85
+ current = 0.0
86
+ steps.times do |step|
87
+ done = (step+1).to_f / steps
88
+ scroll = ((done - current)*amount).floor
89
+ # the fixnum arg represents the number of scroll wheels
90
+ # on the mouse we are simulating (up to 3)
91
+ event = CGEventCreateScrollWheelEvent(nil, units, 1, scroll)
92
+ CGEventPost(KCGHIDEventTap, event)
93
+ sleep QUANTUM
94
+ current += scroll.to_f / amount
95
+ end
96
+ end
97
+
98
+ ##
99
+ # A standard click. Default position is the current position.
100
+ #
101
+ # @param [CGPoint]
102
+ def click point = current_position
103
+ event = CGEventCreateMouseEvent(nil, KCGEventLeftMouseDown, point, KCGMouseButtonLeft)
104
+ CGEventPost(KCGHIDEventTap, event)
105
+ # @todo Should not set number of sleep frames statically.
106
+ 12.times do sleep QUANTUM end
107
+ CGEventSetType(event, KCGEventLeftMouseUp)
108
+ CGEventPost(KCGHIDEventTap, event)
109
+ end
110
+
111
+ ##
112
+ # Standard secondary click. Default position is the current position.
113
+ #
114
+ # @param [CGPoint]
115
+ def secondary_click point = current_position
116
+ event = CGEventCreateMouseEvent(nil, KCGEventRightMouseDown, point, KCGMouseButtonRight)
117
+ CGEventPost(KCGHIDEventTap, event)
118
+ CGEventSetType(event, KCGEventRightMouseUp)
119
+ CGEventPost(KCGHIDEventTap, event)
120
+ end
121
+ alias_method :right_click, :secondary_click
122
+
123
+ ##
124
+ # A standard double click. Defaults to clicking at the current position.
125
+ #
126
+ # @param [CGPoint]
127
+ def double_click point = current_position
128
+ event = CGEventCreateMouseEvent(nil, KCGEventLeftMouseDown, point, KCGMouseButtonLeft)
129
+ CGEventPost(KCGHIDEventTap, event)
130
+ CGEventSetType(event, KCGEventLeftMouseUp)
131
+ CGEventPost(KCGHIDEventTap, event)
132
+
133
+ CGEventSetIntegerValueField(event, KCGMouseEventClickState, 2)
134
+ CGEventSetType(event, KCGEventLeftMouseDown)
135
+ CGEventPost(KCGHIDEventTap, event)
136
+ CGEventSetType(event, KCGEventLeftMouseUp)
137
+ CGEventPost(KCGHIDEventTap, event)
138
+ end
139
+
140
+ ##
141
+ # Click with an arbitrary mouse button, using numbers to represent
142
+ # the mouse button. At the time of writing, the documented values are:
143
+ #
144
+ # - KCGMouseButtonLeft = 0
145
+ # - KCGMouseButtonRight = 1
146
+ # - KCGMouseButtonCenter = 2
147
+ #
148
+ # And the rest are not documented! Though they should be easy enough
149
+ # to figure out. See the `CGMouseButton` enum in the reference
150
+ # documentation for the most up to date list.
151
+ #
152
+ # @param [CGPoint]
153
+ # @param [Number]
154
+ def arbitrary_click point = current_position, button = KCGMouseButtonCenter
155
+ event = CGEventCreateMouseEvent(nil, KCGEventOtherMouseDown, point, button)
156
+ CGEventPost(KCGHIDEventTap, event)
157
+ CGEventSetType(event, KCGEventOtherMouseUp)
158
+ CGEventPost(KCGHIDEventTap, event)
159
+ end
160
+ alias_method :other_click, :arbitrary_click
161
+
162
+
163
+ private
164
+
165
+ ##
166
+ # Executes a mouse movement animation. It can be a simple cursor
167
+ # move or a drag depending on what is passed to `type`.
168
+ def animate type, button, from, to, duration
169
+ steps = (FPS * duration).floor
170
+ xstep = (to.x - from.x) / steps
171
+ ystep = (to.y - from.y) / steps
172
+ steps.times do
173
+ from.x += xstep
174
+ from.y += ystep
175
+ event = CGEventCreateMouseEvent(nil, type, from, button)
176
+ CGEventPost(KCGHIDEventTap, event)
177
+ sleep QUANTUM
178
+ end
179
+ $stderr.puts 'Not moving anywhere' if from == to
180
+ event = CGEventCreateMouseEvent(nil, type, to, button)
181
+ CGEventPost(KCGHIDEventTap, event)
182
+ sleep QUANTUM
183
+ end
184
+
185
+ end
@@ -0,0 +1,15 @@
1
+ require 'rspec/expectations'
2
+
3
+ ##
4
+ # Custom RSpec matchers for AXElements.
5
+ module RSpec::Expectations
6
+
7
+ ##
8
+ # app.window.button.should be_visible
9
+ class BeVisible
10
+ end
11
+
12
+ class BeInFrame
13
+ end
14
+
15
+ end
@@ -0,0 +1,72 @@
1
+ # -*- coding: utf-8 -*-
2
+ class TestAXApplication < TestAX
3
+
4
+ APP = AX::Application.new REF, AX.attrs_of_element(REF)
5
+
6
+ def test_is_a_direct_subclass_of_element
7
+ assert_equal AX::Element, AX::Application.superclass
8
+ end
9
+
10
+ def app inst
11
+ inst.instance_variable_get :@app
12
+ end
13
+
14
+ def test_can_set_focus_to_an_app
15
+ app(APP).hide
16
+ sleep 0.2
17
+ refute APP.active?
18
+ APP.set_attribute :focused, true
19
+ sleep 0.2
20
+ assert APP.active?
21
+ ensure
22
+ app(APP).activateWithOptions NSApplicationActivateIgnoringOtherApps
23
+ end
24
+
25
+ def test_can_hide_the_app
26
+ APP.set_attribute :focused, false
27
+ sleep 0.2
28
+ refute APP.active?
29
+ ensure
30
+ app(APP).activateWithOptions NSApplicationActivateIgnoringOtherApps
31
+ end
32
+
33
+ def test_attribute_has_special_case_for_focused
34
+ assert_instance_of_boolean APP.attribute :focused?
35
+ assert_instance_of_boolean APP.attribute :focused
36
+ end
37
+
38
+ def test_attribute_still_works_for_other_attributes
39
+ assert_equal 'AXElementsTester', APP.title
40
+ end
41
+
42
+ def test_inspect_includes_pid
43
+ assert_match /\spid=\d+/, APP.inspect
44
+ end
45
+
46
+ def test_inspect_includes_focused
47
+ assert_match /\sfocused\[(?:✔|✘)\]/, APP.inspect
48
+ end
49
+
50
+ def test_type_string_forwards_call
51
+ class << AX
52
+ alias_method :old_keyboard_action, :keyboard_action
53
+ def keyboard_action element, string
54
+ true if string == 'test' && element == TestAX::REF
55
+ end
56
+ end
57
+ assert APP.type_string('test')
58
+ ensure
59
+ class << AX; alias_method :keyboard_action, :old_keyboard_action; end
60
+ end
61
+
62
+ def test_terminate_kills_app
63
+ skip 'Not sure how to reset state after this test...'
64
+ assert AX::DOCK.terminate
65
+ end
66
+
67
+ def test_dock_constant_is_set
68
+ assert_instance_of AX::Application, AX::DOCK
69
+ assert_equal 'Dock', AX::DOCK.attribute(:title)
70
+ end
71
+
72
+ end
@@ -0,0 +1,27 @@
1
+ class TestElementsRowChildInColumn < TestAX
2
+
3
+ # these tests depend on Search already working
4
+
5
+ APP = AX::Application.new REF, AX.attrs_of_element(REF)
6
+
7
+ def table
8
+ @@table ||= APP.main_window.table
9
+ end
10
+
11
+ def rows
12
+ @@rows ||= table.rows
13
+ end
14
+
15
+ def test_returns_correct_column
16
+ row = rows.first
17
+ assert_equal row.children.second, row.child_in_column(header: 'Two')
18
+ assert_equal row.children.first, row.child_in_column(header: 'One')
19
+ end
20
+
21
+ def test_raises_seach_failure_if_nothing_found
22
+ assert_raises AX::Element::SearchFailure do
23
+ rows.first.child_in_column(header: 'Fifty')
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,38 @@
1
+ class TestAXSystemWide < MiniTest::Unit::TestCase
2
+
3
+ def test_is_singleton
4
+ assert_raises NoMethodError do
5
+ AX::SystemWide.new
6
+ end
7
+ assert_respond_to AX::SystemWide, :instance
8
+ end
9
+
10
+ def test_type_string_makes_appropriate_callback
11
+ class << AX
12
+ alias_method :old_keyboard_action, :keyboard_action
13
+ def keyboard_action element, string
14
+ true if string == 'test' && element == AXUIElementCreateSystemWide()
15
+ end
16
+ end
17
+ assert AX::SYSTEM.type_string('test')
18
+ ensure
19
+ class << AX; alias_method :keyboard_action, :old_keyboard_action; end
20
+ end
21
+
22
+ def test_search_not_allowed
23
+ assert_raises NoMethodError do
24
+ AX::SYSTEM.search
25
+ end
26
+ end
27
+
28
+ def test_notifications_not_allowed
29
+ assert_raises NoMethodError do
30
+ AX::SYSTEM.search
31
+ end
32
+ end
33
+
34
+ def test_expose_instance_as_constant
35
+ assert_instance_of AX::SystemWide, AX::SYSTEM
36
+ end
37
+
38
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,119 @@
1
+ require 'rubygems'
2
+
3
+ require 'ax_elements'
4
+ require 'stringio'
5
+
6
+ # Accessibility.log.level = Logger::DEBUG
7
+
8
+ # We want to launch the test app and make sure it responds to
9
+ # accessibility queries, but that is difficult, so we just sleep
10
+ APP_BUNDLE_URL = NSURL.URLWithString File.expand_path './test/fixture/Release/AXElementsTester.app'
11
+
12
+ error = Pointer.new :id
13
+ TEST_APP = NSWorkspace.sharedWorkspace.launchApplicationAtURL APP_BUNDLE_URL,
14
+ options: NSWorkspaceLaunchAsync,
15
+ configuration: {},
16
+ error: error
17
+ if error[0]
18
+ $stderr.puts 'You need to build AND run the fixture app before running tests'
19
+ $stderr.puts 'Run `rake fixture`'
20
+ exit 3
21
+ else
22
+ sleep 3 # I haven't yet figured out a good way of knowing exactly
23
+ # when the app is ready
24
+ # Make sure the test app is closed when testing finishes
25
+ at_exit do TEST_APP.terminate end
26
+ end
27
+
28
+
29
+ gem 'minitest'
30
+ require 'minitest/autorun'
31
+
32
+ # preprocessor powers, assemble!
33
+ if ENV['BENCH']
34
+ require 'minitest/benchmark'
35
+ else
36
+ require'minitest/pride'
37
+ end
38
+
39
+
40
+ class MiniTest::Unit::TestCase
41
+ # You may need this to help track down an issue if a test is crashing MacRuby
42
+ # def self.test_order
43
+ # :alpha
44
+ # end
45
+
46
+ def assert_instance_of_boolean value
47
+ message = "Expected #{value.inspect} to be a boolean"
48
+ assert value.is_a?(TrueClass) || value.is_a?(FalseClass), message
49
+ end
50
+
51
+ def self.bench_range
52
+ bench_exp 10, 10_000
53
+ end
54
+ end
55
+
56
+ ##
57
+ # A mix in module to allow capture of logs
58
+ module LoggingCapture
59
+ def setup
60
+ super
61
+ @log_output = StringIO.new
62
+ Accessibility.log = Logger.new @log_output
63
+ end
64
+ end
65
+
66
+ module AXHelpers
67
+ def pid_for name
68
+ NSWorkspace.sharedWorkspace.runningApplications.find do |app|
69
+ app.bundleIdentifier == name
70
+ end.processIdentifier
71
+ end
72
+
73
+ # returns raw attribute
74
+ def attribute_for element, attr
75
+ ptr = Pointer.new :id
76
+ AXUIElementCopyAttributeValue(element, attr, ptr)
77
+ ptr[0]
78
+ end
79
+
80
+ def children_for element
81
+ attribute_for element, KAXChildrenAttribute
82
+ end
83
+
84
+ def value_for element
85
+ attribute_for element, KAXValueAttribute
86
+ end
87
+
88
+ def action_for element, action
89
+ AXUIElementPerformAction(element, action)
90
+ end
91
+
92
+ # remember to wrap structs in an AXValueRef
93
+ def set_attribute_for element, attr, value
94
+ AXUIElementSetAttributeValue(element, attr, value)
95
+ end
96
+ end
97
+
98
+ class TestAX < MiniTest::Unit::TestCase
99
+ include AXHelpers
100
+ extend AXHelpers
101
+
102
+ APP_BUNDLE_IDENTIFIER = 'com.marketcircle.AXElementsTester'
103
+ PID = pid_for APP_BUNDLE_IDENTIFIER
104
+ REF = AXUIElementCreateApplication(PID)
105
+
106
+ # execute the block with full logging turned on
107
+ def with_logging level = Logger::DEBUG
108
+ original_level = Accessibility.log.level
109
+ Accessibility.log.level = level
110
+ yield
111
+ Accessibility.log.level = original_level
112
+ end
113
+ end
114
+
115
+ ##
116
+ # Just pretend that you didnt' see this hack
117
+ class AX::Element
118
+ attr_reader :ref
119
+ end
@@ -0,0 +1,127 @@
1
+ class TestAccessibility < TestAX
2
+
3
+ APP = AX::Application.new REF, AX.attrs_of_element(REF)
4
+
5
+ def close_button
6
+ @@button ||= APP.attribute(:main_window).attribute(:children).find do |item|
7
+ item.class == AX::CloseButton
8
+ end
9
+ end
10
+
11
+ def test_path_returns_correct_elements_in_correct_order
12
+ list = Accessibility.path(APP.main_window.close_button)
13
+ assert_equal 3, list.size
14
+ assert_instance_of AX::CloseButton, list.first
15
+ assert_instance_of AX::StandardWindow, list.second
16
+ assert_instance_of AX::Application, list.third
17
+ end
18
+
19
+ def test_graph
20
+ skip 'ZOMG, yeah right'
21
+ end
22
+
23
+ def test_dump_works_for_nested_tab_groups
24
+ element = APP.main_window.children.find { |item| item.role == KAXTabGroupRole }
25
+ output = Accessibility.dump(element)
26
+
27
+ refute_empty output
28
+
29
+ expected = [
30
+ ['AX::TabGroup', 0],
31
+ ['AX::RadioButton', 1], ['AX::RadioButton', 1], ['AX::TabGroup', 1],
32
+ ['AX::RadioButton', 2], ['AX::RadioButton', 2], ['AX::TabGroup', 2],
33
+ ['AX::RadioButton', 3], ['AX::RadioButton', 3], ['AX::TabGroup', 3],
34
+ ['AX::RadioButton', 4], ['AX::RadioButton', 4],
35
+ ['AX::Group', 4],
36
+ ['AX::TextField', 5], ['AX::StaticText', 6],
37
+ ['AX::TextField' , 5], ['AX::StaticText', 6]
38
+ ]
39
+
40
+ output = output.split("\n")
41
+
42
+ until output.empty?
43
+ actual_line = output.shift
44
+ expected_klass, indents = expected.shift
45
+ assert_equal indents, actual_line.match(/^\t*/).to_a.first.length, actual_line
46
+ actual_line.strip!
47
+ assert_match /^\#<#{expected_klass}/, actual_line
48
+ end
49
+ end
50
+
51
+ def test_returns_some_kind_of_ax_element
52
+ assert_kind_of AX::Element, Accessibility.element_under_mouse
53
+ end
54
+
55
+ def test_returns_element_under_the_mouse
56
+ button = APP.main_window.close_button
57
+ Mouse.move_to button.to_point, 0
58
+ # sleep 0.05 # how often will this fail without waiting?
59
+ assert_equal button, Accessibility.element_under_mouse
60
+ end
61
+
62
+ def test_element_at_point_returns_button_when_given_buttons_coordinates
63
+ point = close_button.position
64
+ assert_equal close_button, Accessibility.element_at_point(*point.to_a)
65
+ assert_equal close_button, Accessibility.element_at_point(point.to_a)
66
+ assert_equal close_button, Accessibility.element_at_point(point)
67
+ end
68
+
69
+ def test_elemnent_at_point_is_element_at_position
70
+ assert_equal Accessibility.method(:element_at_point), Accessibility.method(:element_at_position)
71
+ end
72
+
73
+ def test_application_with_name_gets_app_if_running
74
+ ret = Accessibility.application_with_name 'Dock'
75
+ assert_instance_of AX::Application, ret
76
+ assert_equal 'Dock', ret.title
77
+ end
78
+
79
+ def test_application_with_name_gets_nil_if_not_found
80
+ assert_nil Accessibility.application_with_name('App That Does Not Exist')
81
+ end
82
+
83
+ def test_application_with_name_called_before_and_after_app_is_running
84
+ skip 'This is a bug that was fixed but should have a test'
85
+ end
86
+
87
+ def test_app_with_bundle_id_returns_the_correct_app
88
+ ret = Accessibility.application_with_bundle_identifier(APP_BUNDLE_IDENTIFIER)
89
+ assert_instance_of AX::Application, ret
90
+ assert_equal APP, ret
91
+ end
92
+
93
+ def test_app_with_bundle_id_return_app_if_app_is_running
94
+ app = Accessibility.application_with_bundle_identifier 'com.apple.dock'
95
+ assert_equal 'Dock', app.attribute(:title)
96
+ end
97
+
98
+ # @todo how do we test when app is not already running?
99
+
100
+ def test_launches_app_if_it_is_not_running
101
+ def grabbers
102
+ NSRunningApplication.runningApplicationsWithBundleIdentifier( 'com.apple.Grab' )
103
+ end
104
+ grabbers.each do |dude| dude.terminate end
105
+ assert_empty grabbers
106
+ Accessibility.application_with_bundle_identifier( 'com.apple.Grab' )
107
+ refute_empty grabbers
108
+ ensure
109
+ grabbers.each do |dude| dude.terminate end
110
+ end
111
+
112
+ def test_app_with_bundle_id_times_out_if_app_cannot_be_launched
113
+ skip 'This is difficult to do...'
114
+ end
115
+
116
+ def test_app_with_bundle_id_allows_override_of_the_sleep_time
117
+ skip 'This is difficult to test...'
118
+ end
119
+
120
+ # @note a bad pid will crash MacRuby
121
+ def test_application_with_pid_gives_me_the_application
122
+ pid = APP.pid
123
+ app = Accessibility.application_with_pid(pid)
124
+ assert_equal APP, app
125
+ end
126
+
127
+ end
@@ -0,0 +1,26 @@
1
+ class TestBlankPredicate < TestAX
2
+
3
+ def test_nil_returns_true
4
+ assert_equal true, nil.blank?
5
+ end
6
+
7
+ def test_nsarray_responds
8
+ assert_respond_to NSArray.array, :blank?
9
+ end
10
+
11
+ def test_nsarray_uses_alias_to_empty?
12
+ ary = NSArray.array
13
+ assert_equal ary.method(:empty?), ary.method(:blank?)
14
+ end
15
+
16
+ def test_element_always_returns_false
17
+ app = AX::Element.new REF, AX.attrs_of_element(REF)
18
+ window = app.attribute(:main_window)
19
+ assert_equal false, window.blank?
20
+ assert_equal false, app.blank?
21
+ end
22
+
23
+ # other objects do not implement the method because it is not
24
+ # useful for them to
25
+
26
+ end