AXElements 0.6.0beta1

Sign up to get free protection for your applications and to get access to all the features.
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