AXElements 0.9.0 → 1.0.0.alpha

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 (60) hide show
  1. data/.yardopts +0 -4
  2. data/README.markdown +22 -17
  3. data/Rakefile +1 -1
  4. data/ext/accessibility/key_coder/extconf.rb +1 -1
  5. data/ext/accessibility/key_coder/key_coder.c +2 -4
  6. data/lib/accessibility.rb +3 -3
  7. data/lib/accessibility/core.rb +948 -0
  8. data/lib/accessibility/dsl.rb +30 -186
  9. data/lib/accessibility/enumerators.rb +1 -0
  10. data/lib/accessibility/factory.rb +78 -134
  11. data/lib/accessibility/graph.rb +5 -9
  12. data/lib/accessibility/highlighter.rb +86 -0
  13. data/lib/accessibility/{pretty_printer.rb → pp_inspector.rb} +4 -3
  14. data/lib/accessibility/qualifier.rb +3 -5
  15. data/lib/accessibility/screen_recorder.rb +217 -0
  16. data/lib/accessibility/statistics.rb +57 -0
  17. data/lib/accessibility/translator.rb +23 -32
  18. data/lib/accessibility/version.rb +2 -22
  19. data/lib/ax/application.rb +20 -159
  20. data/lib/ax/element.rb +42 -32
  21. data/lib/ax/scroll_area.rb +5 -6
  22. data/lib/ax/systemwide.rb +1 -33
  23. data/lib/ax_elements.rb +1 -9
  24. data/lib/ax_elements/core_graphics_workaround.rb +5 -0
  25. data/lib/ax_elements/nsarray_compat.rb +17 -97
  26. data/lib/ax_elements/vendor/inflection_data.rb +66 -0
  27. data/lib/ax_elements/vendor/inflections.rb +176 -0
  28. data/lib/ax_elements/vendor/inflector.rb +306 -0
  29. data/lib/minitest/ax_elements.rb +180 -0
  30. data/lib/mouse.rb +227 -0
  31. data/lib/rspec/expectations/ax_elements.rb +234 -0
  32. data/rakelib/gem.rake +3 -12
  33. data/rakelib/test.rake +15 -0
  34. data/test/helper.rb +20 -10
  35. data/test/integration/accessibility/test_core.rb +18 -0
  36. data/test/integration/accessibility/test_dsl.rb +40 -38
  37. data/test/integration/accessibility/test_enumerators.rb +1 -0
  38. data/test/integration/accessibility/test_graph.rb +0 -1
  39. data/test/integration/accessibility/test_qualifier.rb +2 -2
  40. data/test/integration/ax/test_application.rb +2 -9
  41. data/test/integration/ax/test_element.rb +0 -40
  42. data/test/integration/minitest/test_ax_elements.rb +89 -0
  43. data/test/integration/rspec/expectations/test_ax_elements.rb +102 -0
  44. data/test/sanity/accessibility/test_factory.rb +2 -2
  45. data/test/sanity/accessibility/test_highlighter.rb +56 -0
  46. data/test/sanity/accessibility/{test_pretty_printer.rb → test_pp_inspector.rb} +9 -9
  47. data/test/sanity/accessibility/test_statistics.rb +57 -0
  48. data/test/sanity/ax/test_application.rb +1 -16
  49. data/test/sanity/ax/test_element.rb +2 -2
  50. data/test/sanity/ax_elements/test_nsobject_inspect.rb +2 -4
  51. data/test/sanity/minitest/test_ax_elements.rb +17 -0
  52. data/test/sanity/rspec/expectations/test_ax_elements.rb +15 -0
  53. data/test/sanity/test_mouse.rb +22 -0
  54. data/test/test_core.rb +454 -0
  55. metadata +44 -69
  56. data/History.markdown +0 -41
  57. data/lib/accessibility/system_info.rb +0 -230
  58. data/lib/ax_elements/active_support_selections.rb +0 -10
  59. data/lib/ax_elements/mri.rb +0 -57
  60. data/test/sanity/accessibility/test_version.rb +0 -15
@@ -47,7 +47,7 @@ class Accessibility::Graph
47
47
 
48
48
  def identifier
49
49
  klass = @element.class.to_s.split(NAMESPACE).last
50
- ident = @element.pp_identifier.to_s.dup
50
+ ident = @element.pp_identifier.dup
51
51
  if ident.length > 12
52
52
  ident = "#{ident[0...12]}..."
53
53
  end
@@ -58,13 +58,9 @@ class Accessibility::Graph
58
58
  end
59
59
 
60
60
  def shape
61
- if @element.attributes.include?(:focused) && @element.attribute(:focused)
62
- OCTAGON
63
- elsif @element.actions.empty?
64
- OVAL
65
- else
66
- BOX
67
- end
61
+ (@element.attribute(:focused) && OCTAGON) ||
62
+ (@element.actions.empty? && OVAL) ||
63
+ BOX
68
64
  end
69
65
 
70
66
  def style
@@ -73,7 +69,7 @@ class Accessibility::Graph
73
69
  return FILLED unless @element.attribute(:enabled)
74
70
  end
75
71
  # bold if focused and no children
76
- if @element.attributes.include?(:focused) && @element.attribute(:focused)
72
+ if @element.attribute(:focused)
77
73
  return BOLD if @element.size_of(:children).zero?
78
74
  end
79
75
  SOLID
@@ -0,0 +1,86 @@
1
+ framework 'AppKit'
2
+ require 'accessibility/version'
3
+
4
+ ##
5
+ # A screen highlighter for debugging. When you initialize a highligter
6
+ # object it will highlight the given bounds on the screen.
7
+ #
8
+ # Highligter objects can have their colour configured at initialization,
9
+ # and can also have a timeout to automatically stop displaying.
10
+ #
11
+ # @example
12
+ #
13
+ # h = Accessibility::Highlighter.new(CGRectMake(100,100,100,100))
14
+ # # when you are done...
15
+ # h.stop
16
+ #
17
+ class Accessibility::Highlighter < NSWindow
18
+
19
+ # @param bounds [CGRect]
20
+ # @param opts [Hash]
21
+ # @option opts [Number] :timeout
22
+ # @option opts [NSColor] :colour (NSColor.magentaColor)
23
+ def initialize bounds, opts = {}
24
+ colour = opts[:colour] || opts[:color] || NSColor.magentaColor
25
+
26
+ bounds.flip! # we assume the rect is in the other co-ordinate system
27
+
28
+ initWithContentRect bounds,
29
+ styleMask: NSBorderlessWindowMask,
30
+ backing: NSBackingStoreBuffered,
31
+ defer: true
32
+ setOpaque false
33
+ setAlphaValue 0.20
34
+ setLevel NSStatusWindowLevel
35
+ setBackgroundColor colour
36
+ setIgnoresMouseEvents true
37
+ setFrame bounds, display: false
38
+ makeKeyAndOrderFront NSApp
39
+
40
+ if opts.has_key? :timeout
41
+ Dispatch::Queue.new(queue_id).after opts[:timeout] do
42
+ self.stop
43
+ end
44
+ end
45
+ end
46
+
47
+ ##
48
+ # Tell the highlighter to stop displaying.
49
+ #
50
+ # @return [self]
51
+ def stop
52
+ close
53
+ end
54
+
55
+
56
+ private
57
+
58
+ def queue_id
59
+ "com.marketcircle.axelements.window_killer_#{hash}"
60
+ end
61
+ end
62
+
63
+
64
+ ##
65
+ # AXElements extensions to `CGRect`.
66
+ class CGRect
67
+ ##
68
+ # Treats the rect as belonging to one co-ordinate system and then
69
+ # converts it to the other system.
70
+ #
71
+ # This is useful because accessibility API's expect to work with
72
+ # the flipped co-ordinate system (origin in top left), but AppKit
73
+ # prefers to use the cartesian co-ordinate system (origin in bottom
74
+ # left).
75
+ #
76
+ # @return [CGRect]
77
+ def flip!
78
+ screen_height = NSMaxY(NSScreen.mainScreen.frame)
79
+ origin.y = screen_height - NSMaxY(self)
80
+ self
81
+ end
82
+ end
83
+
84
+
85
+ # Initialize the shared application so that windows can be created
86
+ NSApplication.sharedApplication
@@ -10,13 +10,13 @@
10
10
  # - `#attribute` returns the value of a given attribute
11
11
  # - `#size_of` returns the size for an attribute
12
12
  #
13
- module Accessibility::PrettyPrinter
13
+ module Accessibility::PPInspector
14
14
 
15
15
  ##
16
16
  # Create an identifier for the receiver by using various attributes
17
17
  # that should make it very easy to identify the element.
18
18
  #
19
- # @return [String,#to_s]
19
+ # @return [String]
20
20
  def pp_identifier
21
21
  # @note use, or lack of use, of #inspect is intentional for visual effect
22
22
 
@@ -49,7 +49,8 @@ module Accessibility::PrettyPrinter
49
49
  return " id=#{attribute(:identifier)}"
50
50
  end
51
51
 
52
- rescue NoMethodError
52
+ # @todo should we have other fallbacks?
53
+ return EMPTY_STRING
53
54
  end
54
55
 
55
56
  ##
@@ -1,6 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
-
3
- require 'active_support/core_ext/object/blank'
4
2
  require 'accessibility/translator'
5
3
 
6
4
  ##
@@ -31,7 +29,7 @@ class Accessibility::Qualifier
31
29
  @klass = TRANSLATOR.classify(klass)
32
30
  @criteria = criteria
33
31
  @block = Proc.new if block_given?
34
- compile!
32
+ compile criteria
35
33
  end
36
34
 
37
35
  ##
@@ -65,8 +63,8 @@ class Accessibility::Qualifier
65
63
  # {#qualifies?}.
66
64
  #
67
65
  # @param criteria [Hash]
68
- def compile!
69
- @filters = @criteria.map do |key, value|
66
+ def compile criteria
67
+ @filters = criteria.map do |key, value|
70
68
  if value.kind_of? Hash
71
69
  [:subsearch, key, value]
72
70
  elsif key.kind_of? Array
@@ -0,0 +1,217 @@
1
+ framework 'AVFoundation'
2
+ require 'accessibility/version'
3
+ require 'ax_elements/core_graphics_workaround'
4
+
5
+ ##
6
+ # Screen recordings, easy as pie.
7
+ #
8
+ # Things that you need to be concerned about:
9
+ # - screen going to sleep
10
+ # - short recordings (~1 second) don't work too well; it looks like
11
+ # the last bit of the buffer does not get saved so the last ~0.5
12
+ # seconds are not saved to disk (we could add a 0.5 second sleep)
13
+ # - small memory leak when a recording starts on Mountain Lion (GC)
14
+ # - constantly leaking memory during recording on Lion (GC)
15
+ # - run loop hack is not needed if code is already being called from
16
+ # in a run loop
17
+ # - pausing is not working...not sure why
18
+ #
19
+ class Accessibility::ScreenRecorder
20
+
21
+ ##
22
+ # Record the screen while executing the given block. The path to the
23
+ # recording will be returned.
24
+ #
25
+ # The recorder object is yielded.
26
+ #
27
+ # @yield
28
+ # @yieldparam recorder [ScreenRecorder]
29
+ # @return [String]
30
+ def self.record file_name = nil
31
+ raise 'block required' unless block_given?
32
+
33
+ recorder = new
34
+ file_name ? recorder.start(file_name) : recorder.start
35
+ yield recorder
36
+ recorder.file
37
+
38
+ ensure
39
+ recorder.stop
40
+ end
41
+
42
+ ##
43
+ # Path to the screen recording. This is `nil` until the screen
44
+ # recording begins.
45
+ #
46
+ # @return [String]
47
+ attr_reader :file
48
+
49
+ ##
50
+ # @todo Expose configuration options at initialie time
51
+ def initialize
52
+ @session = AVCaptureSession.alloc.init
53
+
54
+ @input = AVCaptureScreenInput.alloc.initWithDisplayID CGMainDisplayID()
55
+ @input.capturesMouseClicks = true
56
+
57
+ @output = AVCaptureMovieFileOutput.alloc.init
58
+ @output.setDelegate self
59
+
60
+ @session.addInput @input
61
+ @session.addOutput @output
62
+
63
+ @sema = Dispatch::Semaphore.new 0
64
+ end
65
+
66
+ ##
67
+ # Synchrnously start recording. You can optionally specify a file
68
+ # name for the recording; if you do not then a default name will be
69
+ # provided in the form `~/Movies/TestRecording-20121017123230.mov`
70
+ # (the timestamp will be different for you).
71
+ #
72
+ # @param file_name [String]
73
+ def start file_name = default_file_name
74
+ @file = default_file_name
75
+ file_url = NSURL.fileURLWithPath @file, isDirectory: false
76
+
77
+ @session.startRunning
78
+ @output.startRecordingToOutputFileURL file_url,
79
+ recordingDelegate: self
80
+
81
+ @sema.wait
82
+ end
83
+
84
+ ##
85
+ # Whether or not the recording has begun. This will be `true`
86
+ # after calling {#start} until {#stop} is called. It will be
87
+ # `true` while the recording is paused.
88
+ def started?
89
+ @output.recording?
90
+ end
91
+
92
+ # ##
93
+ # # Whether or not the recording has been paused.
94
+ # def paused?
95
+ # @output.paused?
96
+ # end
97
+
98
+ ##
99
+ # Duration of the recording, in seconds.
100
+ #
101
+ # @return [Float]
102
+ def length
103
+ duration = @output.recordedDuration
104
+ (duration.value.to_f / duration.timescale.to_f)
105
+ end
106
+
107
+ ##
108
+ # Size of the recording on disk, in bytes.
109
+ #
110
+ # @return [Fixnum]
111
+ def size
112
+ @output.recordedFileSize
113
+ end
114
+
115
+ # ##
116
+ # # Synchronously pause the recording. You can optionally pass a block
117
+ # # to this method.
118
+ # #
119
+ # # If you pass a block, the recording is paused so that the block
120
+ # # can execute and recording resumes after the block finishes. If
121
+ # # you do not pass a block then the recording is paused until you
122
+ # # call {#resume} on the receiver.
123
+ # #
124
+ # # @yield Optionally pass a block
125
+ # def pause
126
+ # @output.pauseRecording
127
+ # wait_for_callback
128
+ # @sema.wait
129
+
130
+ # if block_given?
131
+ # yield
132
+ # resume
133
+ # end
134
+ # end
135
+
136
+ # ##
137
+ # # Synchronously resume a {#pause}d recording.
138
+ # def resume
139
+ # @output.resumeRecording
140
+ # wait_for_callback
141
+ # @sema.wait
142
+ # end
143
+
144
+ ##
145
+ # Synchronously stop recording and finish up commiting any data to disk.
146
+ # A recording cannot be {#start}ed again after it has been stopped; if
147
+ # you want to pause a recording then you should use {#pause} instead.
148
+ def stop
149
+ @session.stopRunning
150
+ @output.stopRecording
151
+ @sema.wait
152
+ wait_for_callback
153
+ @sema.wait
154
+ end
155
+
156
+
157
+ # @!group AVCaptureFileOutputDelegate
158
+
159
+ def captureOutput captureOutput, didOutputSampleBuffer:sampleBuffer, fromConnection:connection
160
+ # gets called for every chunk of the recording getting committed to disk
161
+ end
162
+
163
+ def captureOutput captureOutput, didDropSampleBuffer:sampleBuffer, fromConnection:connection
164
+ NSLog("Error: dropped same data from recording")
165
+ end
166
+
167
+
168
+ # @!group AVCaptureFileOutputRecordingDelegate
169
+
170
+ def captureOutput captureOutput, didFinishRecordingToOutputFileAtURL:outputFileURL, fromConnections:connections, error:error
171
+ NSLog('Finishing')
172
+ CFRunLoopStop(CFRunLoopGetCurrent())
173
+ @sema.signal
174
+ end
175
+
176
+ def captureOutput captureOutput, didPauseRecordingToOutputFileAtURL:fileURL, fromConnections:connections
177
+ NSLog('Pausing')
178
+ CFRunLoopStop(CFRunLoopGetCurrent())
179
+ @sema.signal
180
+ end
181
+
182
+ def captureOutput captureOutput, didResumeRecordingToOutputFileAtURL:fileURL, fromConnections:connections
183
+ NSLog('Resuming')
184
+ CFRunLoopStop(CFRunLoopGetCurrent())
185
+ @sema.signal
186
+ end
187
+
188
+ def captureOutput captureOutput, didStartRecordingToOutputFileAtURL:fileURL, fromConnections:connections
189
+ NSLog('Starting')
190
+ @sema.signal
191
+ end
192
+
193
+ def captureOutput captureOutput, willFinishRecordingToOutputFileAtURL:fileURL, fromConnections:connections, error:error
194
+ NSLog('Will Finish')
195
+ @sema.signal
196
+ end
197
+
198
+
199
+ private
200
+
201
+ def default_file_name
202
+ date = Time.now.strftime '%Y%m%d%H%M%S'
203
+ File.expand_path("~/Movies/TestRecording-#{date}.mov")
204
+ end
205
+
206
+ def wait_for_callback
207
+ case CFRunLoopRunInMode(KCFRunLoopDefaultMode, 30, false)
208
+ when KCFRunLoopRunStopped
209
+ true
210
+ when KCFRunLoopRunTimedOut
211
+ raise 'did not get callback'
212
+ else
213
+ raise 'unexpected result from waiting for callback'
214
+ end
215
+ end
216
+
217
+ end
@@ -0,0 +1,57 @@
1
+ require 'accessibility/version'
2
+
3
+ class Accessibility::Statistics
4
+
5
+ def initialize
6
+ @stats = Hash.new do |h,k| h[k] = 0 end
7
+ @q = Dispatch::Queue.new "com.marketcircle.axelements.stats"
8
+ end
9
+
10
+ def increment key
11
+ @q.async do
12
+ @stats[key] += 1
13
+ end
14
+ end
15
+
16
+ def to_s
17
+ @q.sync do # must be synchronized
18
+ set_max_length
19
+ @out = output_header << output_body << "\n"
20
+ end
21
+ @out
22
+ end
23
+
24
+
25
+ private
26
+
27
+ def set_max_length
28
+ @max_key_len = @stats.keys.map(&:length).max
29
+ @max_val_len = @stats.values.max.to_s.length
30
+ end
31
+
32
+ def dot key, val
33
+ length = 4
34
+ length += @max_key_len - key.length
35
+ length += @max_val_len - val.to_s.length
36
+ "." * length
37
+ end
38
+
39
+ def output_header
40
+ <<-EOS
41
+ ######################
42
+ # AX Call Statistics #
43
+ ######################
44
+ EOS
45
+ end
46
+
47
+ def output_body
48
+ pairs = @stats.to_a.sort { |x,y| y.last <=> x.last }
49
+ pairs.map do |key, val|
50
+ key.to_s << dot(key,val) << val.to_s
51
+ end.join("\n")
52
+ end
53
+
54
+ end
55
+
56
+ # @return [Accessibility::Statistics]
57
+ STATS = Accessibility::Statistics.new