AXElements 0.9.0 → 1.0.0.alpha

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