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