castanaut 1.0.0

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.
@@ -0,0 +1,353 @@
1
+ module Castanaut
2
+ # The movie class is the containing context within which screenplays are
3
+ # invoked. It provides a number of basic stage directions for your
4
+ # screenplays, and can be extended with plugins.
5
+ class Movie
6
+
7
+ # Runs the "screenplay", which is a file containing Castanaut instructions.
8
+ #
9
+ def initialize(screenplay)
10
+ perms_test
11
+
12
+ if !screenplay || !File.exists?(screenplay)
13
+ raise Castanaut::Exceptions::ScreenplayNotFound
14
+ end
15
+ @screenplay_path = screenplay
16
+
17
+ File.open(FILE_RUNNING, 'w') {|f| f.write('')}
18
+
19
+ begin
20
+ # We run the movie in a separate thread; in the main thread we
21
+ # continue to check the "running" file flag and kill the movie if
22
+ # it is removed.
23
+ movie = Thread.new do
24
+ begin
25
+ eval(IO.read(@screenplay_path), binding)
26
+ rescue => e
27
+ @e = e
28
+ ensure
29
+ File.unlink(FILE_RUNNING) if File.exists?(FILE_RUNNING)
30
+ end
31
+ end
32
+
33
+ while File.exists?(FILE_RUNNING)
34
+ sleep 0.5
35
+ break unless movie.alive?
36
+ end
37
+
38
+ if movie.alive?
39
+ movie.kill
40
+ raise Castanaut::Exceptions::AbortedByUser
41
+ end
42
+
43
+ raise @e if @e
44
+ rescue => e
45
+ puts "ABNORMAL EXIT: #{e.message}\n" + e.backtrace.join("\n")
46
+ ensure
47
+ roll_credits
48
+ File.unlink(FILE_RUNNING) if File.exists?(FILE_RUNNING)
49
+ end
50
+ end
51
+
52
+ # Launch the application matching the string given in the first argument.
53
+ # (This resolution is handled by Applescript.)
54
+ #
55
+ # If the options hash is given, it should contain the co-ordinates for
56
+ # the window (top, left, width, height). The to method will format these
57
+ # co-ordinates appropriately.
58
+ #
59
+ def launch(app_name, *options)
60
+ options = combine_options(*options)
61
+
62
+ ensure_window = ""
63
+ case app_name.downcase
64
+ when "safari"
65
+ ensure_window = "if (count(windows)) < 1 then make new document"
66
+ end
67
+
68
+ positioning = ""
69
+ if options[:to]
70
+ pos = "#{options[:to][:left]}, #{options[:to][:top]}"
71
+ dims = "#{options[:to][:left] + options[:to][:width]}, " +
72
+ "#{options[:to][:top] + options[:to][:height]}"
73
+ if options[:to][:width]
74
+ positioning = "set bounds of front window to {#{pos}, #{dims}}"
75
+ else
76
+ positioning = "set position of front window to {#{pos}}"
77
+ end
78
+ end
79
+
80
+ execute_applescript(%Q`
81
+ tell application "#{app_name}"
82
+ activate
83
+ #{ensure_window}
84
+ #{positioning}
85
+ end tell
86
+ `)
87
+ end
88
+
89
+ # Move the mouse cursor to the specified co-ordinates.
90
+ #
91
+ def cursor(*options)
92
+ options = combine_options(*options)
93
+ apply_offset(options)
94
+ @cursor_loc ||= {}
95
+ @cursor_loc[:x] = options[:to][:left]
96
+ @cursor_loc[:y] = options[:to][:top]
97
+ automatically "mousemove #{@cursor_loc[:x]} #{@cursor_loc[:y]}"
98
+ end
99
+
100
+ alias :move :cursor
101
+
102
+ # Send a mouse-click at the current mouse location.
103
+ #
104
+ def click(btn = 'left')
105
+ automatically "mouseclick #{mouse_button_translate(btn)}"
106
+ end
107
+
108
+ # Send a double-click at the current mouse location.
109
+ #
110
+ def doubleclick(btn = 'left')
111
+ automatically "mousedoubleclick #{mouse_button_translate(btn)}"
112
+ end
113
+
114
+ # Send a triple-click at the current mouse location.
115
+ #
116
+ def tripleclick(btn = 'left')
117
+ automatically "mousetripleclick #{mouse_button_translate(btn)}"
118
+ end
119
+
120
+ # Press the button down at the current mouse location. Does not
121
+ # release the button until the mouseup method is invoked.
122
+ #
123
+ def mousedown(btn = 'left')
124
+ automatically "mousedown #{mouse_button_translate(btn)}"
125
+ end
126
+
127
+ # Releases the mouse button pressed by a previous mousedown.
128
+ #
129
+ def mouseup(btn = 'left')
130
+ automatically "mouseup #{mouse_button_translate(btn)}"
131
+ end
132
+
133
+ # "Drags" the mouse by (effectively) issuing a mousedown at the current
134
+ # mouse location, then moving the mouse to the specified coordinates, then
135
+ # issuing a mouseup.
136
+ #
137
+ def drag(*options)
138
+ options = combine_options(*options)
139
+ apply_offset(options)
140
+ automatically "mousedrag #{options[:to][:left]} #{options[:to][:top]}"
141
+ end
142
+
143
+ # Sends the characters into the active control in the active window.
144
+ #
145
+ def type(str)
146
+ automatically "type #{str}"
147
+ end
148
+
149
+ # Sends the keycode (a hex value) to the active control in the active
150
+ # window. For more about keycode values, see Mac Developer documentation.
151
+ #
152
+ def hit(key)
153
+ automatically "hit #{key}"
154
+ end
155
+
156
+ # Don't do anything for the specified number of seconds (can be portions
157
+ # of a second).
158
+ #
159
+ def pause(seconds)
160
+ sleep seconds
161
+ end
162
+
163
+ # Use Leopard's native text-to-speech functionality to emulate a human
164
+ # voice saying the narrative text.
165
+ #
166
+ def say(narrative)
167
+ run(%Q`say "#{escape_dq(narrative)}"`)
168
+ end
169
+
170
+ # Starts saying the narrative text, and simultaneously begins executing
171
+ # the given block. Waits until both are finished.
172
+ #
173
+ def while_saying(narrative)
174
+ if block_given?
175
+ fork { say(narrative) }
176
+ yield
177
+ Process.wait
178
+ else
179
+ say(narrative)
180
+ end
181
+ end
182
+
183
+ # Get a hash representing specific screen co-ordinates. Use in combination
184
+ # with cursor, drag, launch, and similar methods.
185
+ #
186
+ def to(l, t, w = nil, h = nil)
187
+ result = {
188
+ :to => {
189
+ :left => l,
190
+ :top => t
191
+ }
192
+ }
193
+ result[:to][:width] = w if w
194
+ result[:to][:height] = h if h
195
+ result
196
+ end
197
+
198
+ alias :at :to
199
+
200
+ # Get a hash representing specific screen co-ordinates *relative to the
201
+ # current mouse location.
202
+ #
203
+ def by(x, y)
204
+ unless @cursor_loc
205
+ @cursor_loc = automatically("mouselocation").strip.split(' ')
206
+ @cursor_loc = {:x => @cursor_loc[0].to_i, :y => @cursor_loc[1].to_i}
207
+ end
208
+ to(@cursor_loc[:x] + x, @cursor_loc[:y] + y)
209
+ end
210
+
211
+ # The result of this method can be added +to+ a co-ordinates hash,
212
+ # offsetting the top and left values by the given margins.
213
+ #
214
+ def offset(x, y)
215
+ { :offset => { :x => x, :y => y } }
216
+ end
217
+
218
+
219
+ # Returns a region hash describing the entire screen area. (May be wonky
220
+ # for multi-monitor set-ups.)
221
+ #
222
+ def screen_size
223
+ coords = execute_applescript(%Q`
224
+ tell application "Finder"
225
+ get bounds of window of desktop
226
+ end tell
227
+ `)
228
+ coords = coords.split(", ").collect {|c| c.to_i}
229
+ to(*coords)
230
+ end
231
+
232
+ # Runs a shell command, performing fairly naive (but effective!) exit
233
+ # status handling. Returns the stdout result of the command.
234
+ #
235
+ def run(cmd)
236
+ #puts("Executing: #{cmd}")
237
+ result = `#{cmd}`
238
+ raise Castanaut::Exceptions::ExternalActionError if $?.exitstatus > 0
239
+ result
240
+ end
241
+
242
+ # Adds custom methods to this movie instance, allowing you to perform
243
+ # additional actions. See the README.txt for more information.
244
+ #
245
+ def plugin(str)
246
+ str.downcase!
247
+ begin
248
+ require File.join(File.dirname(@screenplay_path),"plugins","#{str}.rb")
249
+ rescue LoadError
250
+ require File.join(LIBPATH, "plugins", "#{str}.rb")
251
+ end
252
+ extend eval("Castanaut::Plugin::#{str.capitalize}")
253
+ end
254
+
255
+ # Loads a script from a file into a string, looking first in the
256
+ # scripts directory beneath the path where Castanaut was executed,
257
+ # and falling back to Castanaut's gem path.
258
+ #
259
+ def script(filename)
260
+ @cached_scripts ||= {}
261
+ unless @cached_scripts[filename]
262
+ fpath = File.join(File.dirname(@screenplay_path), "scripts", filename)
263
+ scpt = nil
264
+ if File.exists?(fpath)
265
+ scpt = IO.read(fpath)
266
+ else
267
+ scpt = IO.read(File.join(PATH, "scripts", filename))
268
+ end
269
+ @cached_scripts[filename] = scpt
270
+ end
271
+
272
+ @cached_scripts[filename]
273
+ end
274
+
275
+ # This stage direction is slightly different to the other ones. It collects
276
+ # a set of directions to be executed when the movie ends, or when it is
277
+ # aborted by the user. Mostly, it's used for cleaning up stuff. Here's
278
+ # an example:
279
+ #
280
+ # ishowu_start_recording
281
+ # at_end_of_movie do
282
+ # ishowu_stop_recording
283
+ # end
284
+ # move to(100, 100) # ... et cetera
285
+ #
286
+ # You can use this multiple times in your screenplay -- remember that if
287
+ # the movie is aborted by the user before this direction is used, its
288
+ # contents won't be executed. So in general, create an at_end_of_movie
289
+ # block after every action that you want to revert (like in the example
290
+ # above).
291
+ def at_end_of_movie(&blk)
292
+ @end_credits ||= []
293
+ @end_credits << blk
294
+ end
295
+
296
+ protected
297
+ def execute_applescript(scpt)
298
+ File.open(FILE_APPLESCRIPT, 'w') {|f| f.write(scpt)}
299
+ result = run("osascript #{FILE_APPLESCRIPT}")
300
+ File.unlink(FILE_APPLESCRIPT)
301
+ result
302
+ end
303
+
304
+ def automatically(cmd)
305
+ run("#{osxautomation_path} \"#{cmd}\"")
306
+ end
307
+
308
+ def escape_dq(str)
309
+ str.gsub(/\\/,'\\\\\\').gsub(/"/, '\"')
310
+ end
311
+
312
+ def combine_options(*args)
313
+ options = args.inject({}) { |result, option| result.update(option) }
314
+ end
315
+
316
+ private
317
+ def osxautomation_path
318
+ File.join(PATH, "cbin", "osxautomation")
319
+ end
320
+
321
+ def perms_test
322
+ return if File.executable?(osxautomation_path)
323
+ puts "IMPORTANT: Castanaut has recently been installed or updated. " +
324
+ "You need to give it the right to control mouse and keyboard " +
325
+ "input during screenplays."
326
+
327
+ run("sudo chmod a+x #{osxautomation_path}")
328
+
329
+ if File.executable?(osxautomation_path)
330
+ puts "Permission granted. Thanks."
331
+ else
332
+ raise Castanaut::Exceptions::OSXAutomationPermissionError
333
+ end
334
+ end
335
+
336
+ def apply_offset(options)
337
+ return unless options[:to] && options[:offset]
338
+ options[:to][:left] += options[:offset][:x] || 0
339
+ options[:to][:top] += options[:offset][:y] || 0
340
+ end
341
+
342
+ def mouse_button_translate(btn)
343
+ return btn if btn.is_a?(Integer)
344
+ {"left" => 1, "right" => 2, "middle" => 3}[btn]
345
+ end
346
+
347
+ def roll_credits
348
+ return unless @end_credits && @end_credits.any?
349
+ @end_credits.each {|credit| credit.call}
350
+ end
351
+
352
+ end
353
+ end
@@ -0,0 +1,26 @@
1
+ module Castanaut
2
+
3
+ # Castanaut uses plugins to extend the available actions beyond simple
4
+ # mouse and keyboard input. Typically each plugin is application-specific.
5
+ # See the Safari, Mousepose and Ishowu plugins for examples, and review the
6
+ # README.txt for details on creating your own.
7
+ #
8
+ # In short, for a plugin called "foo", your script should have this structure:
9
+ #
10
+ # module Castanaut
11
+ # module Plugin
12
+ # module Foo
13
+ #
14
+ # # define your stage directions (ie, Movie instance methods) here.
15
+ #
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ # The script must exist in a sub-directory of the screenplay's location
21
+ # called "plugins", and must be called (in this case): foo.rb.
22
+ #
23
+ module Plugin
24
+ end
25
+
26
+ end
data/lib/castanaut.rb ADDED
@@ -0,0 +1,64 @@
1
+ # $Id$
2
+
3
+ # Equivalent to a header guard in C/C++
4
+ # Used to prevent the class/module from being loaded more than once
5
+ unless defined? Castanaut
6
+
7
+ # The Castanaut module. For orienting yourself within the code, it's
8
+ # recommended you begin with the documentation for the Movie class,
9
+ # which is the big one.
10
+ #
11
+ # Execution typically begins with the Main class.
12
+ module Castanaut
13
+
14
+ # :stopdoc:
15
+ VERSION = '1.0.0'
16
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
17
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
18
+
19
+ FILE_RUNNING = "/tmp/castanaut.running"
20
+ FILE_APPLESCRIPT = "/tmp/castanaut.scpt"
21
+ # :startdoc:
22
+
23
+ # Returns the version string for the library.
24
+ #
25
+ def self.version
26
+ VERSION
27
+ end
28
+
29
+ # Returns the library path for the module. If any arguments are given,
30
+ # they will be joined to the end of the libray path using
31
+ # <tt>File.join</tt>.
32
+ #
33
+ def self.libpath( *args )
34
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, *args)
35
+ end
36
+
37
+ # Returns the lpath for the module. If any arguments are given,
38
+ # they will be joined to the end of the path using
39
+ # <tt>File.join</tt>.
40
+ #
41
+ def self.path( *args )
42
+ args.empty? ? PATH : ::File.join(PATH, *args)
43
+ end
44
+
45
+ # Utility method used to rquire all files ending in .rb that lie in the
46
+ # directory below this file that has the same name as the filename passed
47
+ # in. Optionally, a specific _directory_ name can be passed in such that
48
+ # the _filename_ does not have to be equivalent to the directory.
49
+ #
50
+ def self.require_all_libs_relative_to( fname, dir = nil )
51
+ dir ||= ::File.basename(fname, '.*')
52
+ search_me = ::File.expand_path(
53
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
54
+
55
+ Dir.glob(search_me).sort.each {|rb| require rb}
56
+ end
57
+
58
+ end # module Castanaut
59
+
60
+ Castanaut.require_all_libs_relative_to __FILE__
61
+
62
+ end # unless defined?
63
+
64
+ # EOF
@@ -0,0 +1,94 @@
1
+ module Castanaut; module Plugin
2
+
3
+ # This module provides primitive support for iShowU, a screencast capturing
4
+ # tool for Mac OS X from Shiny White Box.
5
+ #
6
+ # iShowU is widely considered a good, simple application for its purpose,
7
+ # but you're by no means required to use it for Castanaut. Simply write
8
+ # your own module for Snapz Pro, or ScreenFlow, or whatever you like.
9
+ #
10
+ # Shiny White Box is promising much better Applescript support in an
11
+ # imminent version, which could tidy up this module quite a bit.
12
+ #
13
+ # More info: http://www.shinywhitebox.com/home/home.html
14
+ module Ishowu
15
+
16
+ # Set the screencast to capture a particular region of the screen.
17
+ # Generate appropriately-formatted co-ordinates using Castanaut::Movie#to.
18
+ def ishowu_set_region(*options)
19
+ ishowu_applescriptify
20
+
21
+ options = combine_options(*options)
22
+
23
+ ishowu_menu_item("Capture", "Capture full screen")
24
+ sleep(0.2)
25
+ ishowu_menu_item("Capture", "Capture custom area", false)
26
+ sleep(0.2)
27
+ automatically "mousewarp 4 4"
28
+
29
+ drag to(options[:to][:left], options[:to][:top])
30
+
31
+ sleep(0.2)
32
+ bounds = screen_size
33
+ automatically "mousewarp #{bounds[:to][:width]} #{bounds[:to][:height]}"
34
+ drag to(
35
+ options[:to][:left] + options[:to][:width],
36
+ options[:to][:top] + options[:to][:height]
37
+ )
38
+ hit Enter
39
+ ishowu_hide
40
+ end
41
+
42
+ # Tell iShowU to start recording. Will automatically stop recording when
43
+ # the movie is ended, unless you set :auto_stop => false in options.
44
+ def ishowu_start_recording(options = {})
45
+ ishowu_hide
46
+ ishowu_menu_item("Capture", "Start capture")
47
+ unless options[:auto_stop] == false
48
+ at_end_of_movie { ishowu_stop_recording }
49
+ end
50
+ end
51
+
52
+ # Tell iShowU to stop recording.
53
+ def ishowu_stop_recording
54
+ ishowu_menu_item("Capture", "Stop capture")
55
+ end
56
+
57
+ # Execute an iShowU menu option.
58
+ def ishowu_menu_item(menu, item, quiet = true)
59
+ ascript = %Q`
60
+ tell application "iShowU"
61
+ activate
62
+ tell application "System Events"
63
+ click menu item "#{item}" of menu "#{menu}" of menu bar item "#{menu}" of menu bar 1 of process "iShowU"
64
+ #{'set visible of process "iShowU" to false' if quiet}
65
+ end tell
66
+ end
67
+ `
68
+ execute_applescript(ascript)
69
+ end
70
+
71
+ # Hide the iShowU window. This is a bit random, and suggestions are
72
+ # welcomed.
73
+ def ishowu_hide
74
+ ishowu_menu_item("iShowU", "Hide iShowU")
75
+ end
76
+
77
+ private
78
+ # iShowU is not Applescript-enabled out of the box. This fix, arguably
79
+ # a hack, lets us do some limited work with it in Applescript.
80
+ def ishowu_applescriptify
81
+ execute_applescript(%Q`
82
+ try
83
+ tell application "Finder"
84
+ set the a_app to (application file id "com.tcdc.Digitizer") as alias
85
+ end tell
86
+ set the plist_filepath to the quoted form of ¬
87
+ ((POSIX path of the a_app) & "Contents/Info")
88
+ do shell script "defaults write " & the plist_filepath & space ¬
89
+ & "NSAppleScriptEnabled -bool YES"
90
+ end try
91
+ `)
92
+ end
93
+ end
94
+ end; end
@@ -0,0 +1,38 @@
1
+ module Castanaut; module Plugin
2
+
3
+ # This module provides actions for controlling Mousepose, a commercial
4
+ # application from Boinx Software. Basically it lets you put a halo around
5
+ # the mouse whenever a key mouse action occurs.
6
+ #
7
+ # It doesn't do any configuration of Mousepose on the fly. Configure
8
+ # Mousepose settings before running your screenplay.
9
+ #
10
+ # Tested against Mousepose 2. More info: http://www.boinx.com/mousepose
11
+ module Mousepose
12
+
13
+ # Put a halo around the mouse. If a block is given to this method,
14
+ # the halo will be turned off when the block completes. Otherwise,
15
+ # you'll have to use dim to dismiss the halo.
16
+ def highlight
17
+ execute_applescript(%Q`
18
+ tell application "Mousepose"
19
+ start effect
20
+ end
21
+ `)
22
+ if block_given?
23
+ yield
24
+ dim
25
+ end
26
+ end
27
+
28
+ # Dismiss the halo around the mouse that was invoked by a previous
29
+ # highlight method.
30
+ def dim
31
+ execute_applescript(%Q`
32
+ tell application "Mousepose"
33
+ stop effect
34
+ end
35
+ `)
36
+ end
37
+ end
38
+ end; end
@@ -0,0 +1,124 @@
1
+ module Castanaut
2
+
3
+ module Plugin
4
+ # This module provides actions for controlling Safari. It's tested against
5
+ # Safari 3 on Mac OS X 10.5.2.
6
+ module Safari
7
+
8
+ # Open a URL in the front Safari tab.
9
+ def url(str)
10
+ execute_applescript(%Q`
11
+ tell application "safari"
12
+ do JavaScript "location.href = '#{str}'" in front document
13
+ end tell
14
+ `)
15
+ end
16
+
17
+ # Get the co-ordinates of an element in the front Safari tab. Use this
18
+ # with Castanaut::Movie#cursor to send the mouse cursor to the element.
19
+ #
20
+ # Options include:
21
+ # * :index - an integer (*n*) that gets the *n*th element matching the
22
+ # selector. Defaults to the first element.
23
+ # * :area - whereabouts in the element do you want the coordinates.
24
+ # Valid values are: left, center, right, and top, middle, bottom.
25
+ # Defaults to ["center", "middle"].
26
+ # If single axis is given (eg "left"), the other axis uses its default.
27
+ def to_element(selector, options = {})
28
+ pos = options.delete(:area)
29
+ coords = element_coordinates(selector, options)
30
+
31
+ x_axis, y_axis = [:center, :middle]
32
+ [pos].flatten.first(2).each do |p|
33
+ p = p.to_s.downcase
34
+ x_axis = p.to_sym if %w[left center right].include?(p)
35
+ y_axis = p.to_sym if %w[top middle bottom].include?(p)
36
+ end
37
+
38
+ edge_offset = options[:edge_offset] || 3
39
+ case x_axis
40
+ when :left
41
+ x = coords[0] + edge_offset
42
+ when :center
43
+ x = (coords[0] + coords[2] * 0.5).to_i
44
+ when :right
45
+ x = (coords[0] + coords[2]) - edge_offset
46
+ end
47
+
48
+ case y_axis
49
+ when :top
50
+ y = coords[1] + edge_offset
51
+ when :middle
52
+ y = (coords[1] + coords[3] * 0.5).to_i
53
+ when :bottom
54
+ y = (coords[1] + coords[3]) - edge_offset
55
+ end
56
+
57
+ result = { :to => { :left => x, :top => y } }
58
+ end
59
+
60
+ private
61
+ # Note: the script should set the Castanaut.result variable.
62
+ def execute_javascript(scpt)
63
+ execute_applescript %Q`
64
+ tell application "Safari"
65
+ do JavaScript "
66
+ document.oldTitle = document.title;
67
+ #{escape_dq(scpt)}
68
+ if (typeof Castanaut.result != 'undefined') {
69
+ document.title = Castanaut.result;
70
+ }
71
+ " in front document
72
+ set the_result to ((name of window 1) as string)
73
+ do JavaScript "
74
+ document.title = document.oldTitle;
75
+ " in front document
76
+ return the_result
77
+ end tell
78
+ `
79
+ end
80
+
81
+ def element_coordinates(selector, options = {})
82
+ index = options[:index] || 0
83
+ gebys = script('gebys.js')
84
+ cjs = script('coords.js')
85
+ coords = execute_javascript(%Q`
86
+ #{gebys}
87
+ #{cjs}
88
+ Castanaut.result = Castanaut.Coords.forElement(
89
+ '#{selector}',
90
+ #{index}
91
+ );
92
+ `)
93
+
94
+ unless coords.match(/\d+ \d+ \d+ \d+/)
95
+ raise Castanaut::Exceptions::ElementNotFound
96
+ end
97
+
98
+ coords = coords.split(' ').collect {|c| c.to_i}
99
+
100
+ if coords.any? {|c| c < 0 }
101
+ raise Castanaut::Exceptions::ElementOffScreen
102
+ end
103
+
104
+ coords
105
+ end
106
+
107
+ end
108
+ end
109
+
110
+ module Exceptions
111
+ # When getting an element's coordinates, this is raised if no element on
112
+ # the page matches the selector given.
113
+ class ElementNotFound < CastanautError
114
+ end
115
+
116
+ # When getting an element's coordinates, this is raised if the element
117
+ # is found, but cannot be shown on the screen. Normally, we automatically
118
+ # scroll to an element that is currently off-screen, but sometimes that
119
+ # might not be possible (such as if the element is display: none).
120
+ class ElementOffScreen < CastanautError
121
+ end
122
+ end
123
+
124
+ end