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