castanaut 1.0.0

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