Module: Percy
- Defined in:
- lib/percy.rb,
lib/version.rb
Constant Summary collapse
- CLIENT_INFO =
"percy-selenium-ruby/#{VERSION}".freeze
- ENV_INFO =
"selenium/#{Selenium::WebDriver::VERSION} ruby/#{RUBY_VERSION}".freeze
- SESSION_TYPE_AUTOMATE =
'automate'.freeze
- SESSION_TYPE_WEB =
'web'.freeze
- PERCY_DEBUG =
ENV['PERCY_LOGLEVEL'] == 'debug'
- PERCY_SERVER_ADDRESS =
ENV['PERCY_SERVER_ADDRESS'] || 'http://localhost:5338'
- LABEL =
"[\u001b[35m" + (PERCY_DEBUG ? 'percy:ruby' : 'percy') + "\u001b[39m]"
- RESPONSIVE_CAPTURE_SLEEP_TIME =
ENV['RESPONSIVE_CAPTURE_SLEEP_TIME'] || ENV['RESONSIVE_CAPTURE_SLEEP_TIME']
- VERSION =
'1.1.4-beta.1'.freeze
Class Method Summary collapse
- ._clear_cache! ⇒ Object
- .capture_responsive_dom(driver, options, percy_dom_script: nil) ⇒ Object
- .change_window_dimension_and_wait(driver, width, height, resize_count) ⇒ Object
- .create_region(bounding_box: nil, element_xpath: nil, element_css: nil, padding: nil, algorithm: 'ignore', diff_sensitivity: nil, image_ignore_threshold: nil, carousels_enabled: nil, banners_enabled: nil, ads_enabled: nil, diff_ignore_threshold: nil) ⇒ Object
-
.deep_merge_options(base, override) ⇒ Object
Deep-merge
overrideontobase: nested Hashes merge recursively, while arrays and scalars fromoverridereplace those inbase. -
.deep_symbolize(obj) ⇒ Object
Recursively convert all Hash keys (at every nesting level) to symbols so config (string keys from JSON) and per-call options (symbol keys) merge on consistent keys.
- .fetch(url, data = nil) ⇒ Object
- .fetch_percy_dom ⇒ Object
- .get_browser_instance(driver) ⇒ Object
- .get_driver_metadata(driver) ⇒ Object
- .get_element_ids(elements) ⇒ Object
- .get_origin(url) ⇒ Object
- .get_responsive_widths(widths = []) ⇒ Object
- .get_serialized_dom(driver, options, percy_dom_script: nil) ⇒ Object
- .log(msg, lvl = 'info') ⇒ Object
- .percy_enabled? ⇒ Boolean
- .percy_screenshot(driver, name, options = {}) ⇒ Object
- .process_frame(driver, frame_element, options, percy_dom_script) ⇒ Object
- .resolve_readiness_config(options) ⇒ Object
- .responsive_capture_min_height? ⇒ Boolean
- .responsive_capture_reload_page? ⇒ Boolean
- .responsive_snapshot_capture?(options) ⇒ Boolean
- .snapshot(driver, name, options = {}) ⇒ Object
- .unsupported_iframe_src?(src) ⇒ Boolean
-
.wait_for_ready(driver, options) ⇒ Object
Readiness gate: runs PercyDOM.waitForReady via execute_async_script BEFORE serialize.
Class Method Details
._clear_cache! ⇒ Object
578 579 580 581 582 583 584 |
# File 'lib/percy.rb', line 578 def self._clear_cache! @percy_dom = nil @percy_enabled = nil @cli_config = nil @session_type = nil Cache.clear_cache! end |
.capture_responsive_dom(driver, options, percy_dom_script: nil) ⇒ Object
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/percy.rb', line 369 def self.capture_responsive_dom(driver, , percy_dom_script: nil) widths = get_responsive_widths([:widths] || []) dom_snapshots = [] window_size = get_browser_instance(driver).window.size current_width = window_size.width current_height = window_size.height last_window_width = current_width last_window_height = current_height resize_count = 0 driver.execute_script('PercyDOM.waitForResize()') target_height = current_height if responsive_capture_min_height? min = [:minHeight] || @cli_config&.dig('snapshot', 'minHeight') if min target_height = min else log('PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT is enabled but no minHeight value ' \ 'was provided in options or CLI config; using current window height', 'debug',) end end begin widths.each do |width_dict| width = width_dict['width'] height = width_dict['height'] || target_height if last_window_width != width || last_window_height != height resize_count += 1 change_window_dimension_and_wait(driver, width, height, resize_count) last_window_width = width last_window_height = height end if responsive_capture_reload_page? log("Reloading page for width: #{width}", 'debug') begin driver.navigate.refresh rescue StandardError begin driver.driver.browser.navigate.refresh rescue StandardError => e log("Failed to refresh page: #{e}", 'debug') end end percy_dom_script = fetch_percy_dom driver.execute_script(percy_dom_script) driver.execute_script('PercyDOM.waitForResize()') resize_count = 0 end sleep(RESPONSIVE_CAPTURE_SLEEP_TIME.to_i) if RESPONSIVE_CAPTURE_SLEEP_TIME dom_snapshot = get_serialized_dom(driver, , percy_dom_script: percy_dom_script) dom_snapshot['width'] = width dom_snapshots << dom_snapshot end ensure change_window_dimension_and_wait(driver, current_width, current_height, resize_count + 1) end dom_snapshots end |
.change_window_dimension_and_wait(driver, width, height, resize_count) ⇒ Object
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 |
# File 'lib/percy.rb', line 341 def self.change_window_dimension_and_wait(driver, width, height, resize_count) log("Attempting to resize window to #{width}x#{height}", 'debug') begin if driver.capabilities.browser_name == 'chrome' && driver.respond_to?(:execute_cdp) driver.execute_cdp('Emulation.setDeviceMetricsOverride', { height: height, width: width, deviceScaleFactor: 1, mobile: false, },) else get_browser_instance(driver).window.resize_to(width, height) driver.execute_script("window.dispatchEvent(new Event('resize'));") end rescue StandardError => e log("Resizing using cdp failed, falling back to driver for width #{width} #{e}", 'debug') get_browser_instance(driver).window.resize_to(width, height) driver.execute_script("window.dispatchEvent(new Event('resize'));") end begin wait = Selenium::WebDriver::Wait.new(timeout: 1) wait.until { driver.execute_script('return window.resizeCount') == resize_count } actual_size = driver.execute_script('return { w: window.innerWidth, h: window.innerHeight }') log("Resize successful. New Viewport Size: #{actual_size['w']}x#{actual_size['h']}", 'debug') rescue Selenium::WebDriver::Error::TimeoutError log("Timed out waiting for window resize event for width #{width}", 'debug') end end |
.create_region(bounding_box: nil, element_xpath: nil, element_css: nil, padding: nil, algorithm: 'ignore', diff_sensitivity: nil, image_ignore_threshold: nil, carousels_enabled: nil, banners_enabled: nil, ads_enabled: nil, diff_ignore_threshold: nil) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/percy.rb', line 33 def self.create_region( bounding_box: nil, element_xpath: nil, element_css: nil, padding: nil, algorithm: 'ignore', diff_sensitivity: nil, image_ignore_threshold: nil, carousels_enabled: nil, banners_enabled: nil, ads_enabled: nil, diff_ignore_threshold: nil ) element_selector = {} element_selector[:boundingBox] = bounding_box if bounding_box element_selector[:elementXpath] = element_xpath if element_xpath element_selector[:elementCSS] = element_css if element_css region = { algorithm: algorithm, elementSelector: element_selector, } region[:padding] = padding if padding if %w[standard intelliignore].include?(algorithm) configuration = { diffSensitivity: diff_sensitivity, imageIgnoreThreshold: image_ignore_threshold, carouselsEnabled: carousels_enabled, bannersEnabled: , adsEnabled: ads_enabled, }.compact region[:configuration] = configuration unless configuration.empty? end assertion = {} assertion[:diffIgnoreThreshold] = diff_ignore_threshold unless diff_ignore_threshold.nil? region[:assertion] = assertion unless assertion.empty? region end |
.deep_merge_options(base, override) ⇒ Object
Deep-merge override onto base: nested Hashes merge recursively, while
arrays and scalars from override replace those in base.
82 83 84 85 86 |
# File 'lib/percy.rb', line 82 def self.(base, override) base.merge(override) do |_key, old_val, new_val| old_val.is_a?(Hash) && new_val.is_a?(Hash) ? (old_val, new_val) : new_val end end |
.deep_symbolize(obj) ⇒ Object
Recursively convert all Hash keys (at every nesting level) to symbols so config (string keys from JSON) and per-call options (symbol keys) merge on consistent keys. Arrays are walked; scalars are returned as-is.
72 73 74 75 76 77 78 |
# File 'lib/percy.rb', line 72 def self.deep_symbolize(obj) case obj when Hash then obj.each_with_object({}) { |(k, v), h| h[k.to_sym] = deep_symbolize(v) } when Array then obj.map { |e| deep_symbolize(e) } else obj end end |
.fetch(url, data = nil) ⇒ Object
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 |
# File 'lib/percy.rb', line 498 def self.fetch(url, data = nil) uri = URI("#{PERCY_SERVER_ADDRESS}/#{url}") response = if data http = Net::HTTP.new(uri.host, uri.port) http.read_timeout = 600 # seconds request = Net::HTTP::Post.new(uri.path) request.body = data.to_json http.request(request) else Net::HTTP.get_response(uri) end unless response.is_a? Net::HTTPSuccess raise StandardError, "Failed with HTTP error code: #{response.code}" end response end |
.fetch_percy_dom ⇒ Object
476 477 478 479 480 481 |
# File 'lib/percy.rb', line 476 def self.fetch_percy_dom return @percy_dom unless @percy_dom.nil? response = fetch('percy/dom.js') @percy_dom = response.body end |
.get_browser_instance(driver) ⇒ Object
140 141 142 143 144 145 146 |
# File 'lib/percy.rb', line 140 def self.get_browser_instance(driver) if driver.respond_to?(:driver) && driver.driver.respond_to?(:browser) return driver.driver.browser.manage end driver.manage end |
.get_driver_metadata(driver) ⇒ Object
570 571 572 |
# File 'lib/percy.rb', line 570 def self.(driver) DriverMetaData.new(driver) end |
.get_element_ids(elements) ⇒ Object
574 575 576 |
# File 'lib/percy.rb', line 574 def self.get_element_ids(elements) elements.map(&:id) end |
.get_origin(url) ⇒ Object
262 263 264 265 266 267 268 269 270 |
# File 'lib/percy.rb', line 262 def self.get_origin(url) uri = URI.parse(url) raise URI::InvalidURIError, "no host in #{url}" if uri.host.nil? netloc = uri.host.to_s default_ports = {'http' => 80, 'https' => 443} netloc += ":#{uri.port}" if uri.port && uri.port != default_ports[uri.scheme] "#{uri.scheme}://#{netloc}" end |
.get_responsive_widths(widths = []) ⇒ Object
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 |
# File 'lib/percy.rb', line 321 def self.get_responsive_widths(widths = []) begin widths_list = widths.is_a?(Array) ? widths : [] query_param = widths_list.any? ? "?widths=#{widths_list.join(',')}" : '' response = fetch("percy/widths-config#{query_param}") data = JSON.parse(response.body) widths_data = data['widths'] unless widths_data.is_a?(Array) msg = 'Update Percy CLI to the latest version to use responsiveSnapshotCapture' raise StandardError, msg end widths_data rescue StandardError => e log("Failed to get responsive widths: #{e}.", 'debug') raise StandardError, 'Update Percy CLI to the latest version to use ' \ 'responsiveSnapshotCapture' end end |
.get_serialized_dom(driver, options, percy_dom_script: nil) ⇒ Object
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/percy.rb', line 209 def self.get_serialized_dom(driver, , percy_dom_script: nil) # Readiness gate before serialize. Graceful on old CLI. readiness_diagnostics = wait_for_ready(driver, ) # Strip `readiness` from forwarded serialize args -- it's consumed by # wait_for_ready upstream, not a PercyDOM.serialize argument. = .reject { |k, _| k.to_s == 'readiness' } dom_snapshot = driver.execute_script("return PercyDOM.serialize(#{.to_json})") # `!nil?` preserves legitimate falsy returns like {} ("gate ran, no # notable diagnostics"). if !readiness_diagnostics.nil? && dom_snapshot.is_a?(Hash) dom_snapshot['readiness_diagnostics'] = readiness_diagnostics end begin page_origin = get_origin(driver.current_url) iframes = percy_dom_script ? driver.find_elements(:tag_name, 'iframe') : [] if iframes.any? processed_frames = [] iframes.each do |frame| frame_src = frame.attribute('src') next if unsupported_iframe_src?(frame_src) begin frame_origin = get_origin(URI.join(driver.current_url, frame_src).to_s) rescue StandardError => e log("Skipping iframe \"#{frame_src}\": #{e}", 'debug') next end next if frame_origin == page_origin result = process_frame(driver, frame, , percy_dom_script) processed_frames << result if result end dom_snapshot['corsIframes'] = processed_frames if processed_frames.any? end rescue StandardError => e log("Failed to process cross-origin iframes: #{e}", 'debug') begin driver.switch_to.default_content rescue StandardError nil end end dom_snapshot['cookies'] = get_browser_instance(driver). dom_snapshot end |
.log(msg, lvl = 'info') ⇒ Object
483 484 485 486 487 488 489 490 491 492 493 494 495 496 |
# File 'lib/percy.rb', line 483 def self.log(msg, lvl = 'info') msg = "#{LABEL} #{msg}" begin fetch('percy/log', {message: msg, level: lvl}) rescue StandardError => e if PERCY_DEBUG puts "Sending log to CLI Failed #{e}" end ensure if lvl != 'debug' || PERCY_DEBUG puts msg end end end |
.percy_enabled? ⇒ Boolean
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 |
# File 'lib/percy.rb', line 441 def self.percy_enabled? return @percy_enabled unless @percy_enabled.nil? begin response = fetch('percy/healthcheck') version = response['x-percy-core-version'] if version.nil? log('You may be using @percy/agent ' \ 'which is no longer supported by this SDK. ' \ 'Please uninstall @percy/agent and install @percy/cli instead. ' \ 'https://www.browserstack.com/docs/percy/migration/migrate-to-cli') @percy_enabled = false return false end if version.split('.')[0] != '1' log("Unsupported Percy CLI version, #{version}") @percy_enabled = false return false end response_body = JSON.parse(response.body) @cli_config = response_body['config'] @session_type = response_body['type'] @percy_enabled = true true rescue StandardError => e log('Percy is not running, disabling snapshots') log(e, 'debug') @percy_enabled = false false end end |
.percy_screenshot(driver, name, options = {}) ⇒ Object
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 |
# File 'lib/percy.rb', line 518 def self.percy_screenshot(driver, name, = {}) return unless percy_enabled? unless @session_type == SESSION_TYPE_AUTOMATE raise StandardError, 'Invalid function call - percy_screenshot(). ' \ 'Please use percy_snapshot() function for taking screenshot. ' \ 'percy_screenshot() should be used only while using Percy with Automate. ' \ 'For more information on usage of percy_snapshot(), ' \ 'refer doc for your language https://www.browserstack.com/docs/percy/integrate/overview' end begin = .dup = (driver) if .key?(:ignoreRegionSeleniumElements) [:ignore_region_selenium_elements] = .delete(:ignoreRegionSeleniumElements) end if .key?(:considerRegionSeleniumElements) [:consider_region_selenium_elements] = .delete(:considerRegionSeleniumElements) end ignore_region_elements = get_element_ids(.delete(:ignore_region_selenium_elements) || []) consider_region_elements = get_element_ids(.delete(:consider_region_selenium_elements) || []) [:ignore_region_elements] = ignore_region_elements [:consider_region_elements] = consider_region_elements response = fetch('percy/automateScreenshot', client_info: CLIENT_INFO, environment_info: ENV_INFO, sessionId: .session_id, commandExecutorUrl: .command_executor_url, capabilities: .capabilities, snapshotName: name, options: ,) body = JSON.parse(response.body) unless body['success'] raise StandardError, body['error'] end body['data'] rescue StandardError => e log("Could not take Screenshot '#{name}'") log(e, 'debug') end end |
.process_frame(driver, frame_element, options, percy_dom_script) ⇒ Object
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/percy.rb', line 272 def self.process_frame(driver, frame_element, , percy_dom_script) frame_url = frame_element.attribute('src') || 'unknown-src' iframe_snapshot = nil begin driver.switch_to.frame(frame_element) begin driver.execute_script(percy_dom_script) = .merge('enableJavaScript' => true) iframe_snapshot = driver.execute_script("return PercyDOM.serialize(#{.to_json})") rescue StandardError => e log("Failed to process cross-origin frame #{frame_url}: #{e}", 'debug') ensure begin driver.switch_to.default_content rescue StandardError begin driver.switch_to.parent_frame rescue StandardError nil end end end rescue StandardError => e log("Failed to switch to frame #{frame_url}: #{e}", 'debug') begin driver.switch_to.default_content rescue StandardError nil end return nil end return nil if iframe_snapshot.nil? percy_element_id = frame_element.attribute('data-percy-element-id') unless percy_element_id log("Skipping frame #{frame_url}: no matching percyElementId found", 'debug') return nil end { 'iframeData' => {'percyElementId' => percy_element_id}, 'iframeSnapshot' => iframe_snapshot, 'frameUrl' => frame_url, } end |
.resolve_readiness_config(options) ⇒ Object
151 152 153 154 155 156 157 158 159 160 |
# File 'lib/percy.rb', line 151 def self.resolve_readiness_config() global = @cli_config&.dig('snapshot', 'readiness') global = {} unless global.is_a?(Hash) per_snapshot = [:readiness] || ['readiness'] per_snapshot = {} unless per_snapshot.is_a?(Hash) # Normalise symbol keys to strings so the merge collapses :preset and 'preset'. [global, per_snapshot].each_with_object({}) do |hash, merged| hash.each { |k, v| merged[k.to_s] = v } end end |
.responsive_capture_min_height? ⇒ Boolean
27 28 29 30 31 |
# File 'lib/percy.rb', line 27 def self.responsive_capture_min_height? val = ENV['PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT'] || ENV['PERCY_RESONSIVE_CAPTURE_MIN_HEIGHT'] || 'false' val.casecmp('true') == 0 end |
.responsive_capture_reload_page? ⇒ Boolean
21 22 23 24 25 |
# File 'lib/percy.rb', line 21 def self.responsive_capture_reload_page? val = ENV['PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE'] || ENV['PERCY_RESONSIVE_CAPTURE_RELOAD_PAGE'] || 'false' val.casecmp('true') == 0 end |
.responsive_snapshot_capture?(options) ⇒ Boolean
433 434 435 436 437 438 439 |
# File 'lib/percy.rb', line 433 def self.responsive_snapshot_capture?() return false if @cli_config&.dig('percy', 'deferUploads') [:responsive_snapshot_capture] || [:responsiveSnapshotCapture] || @cli_config&.dig('snapshot', 'responsiveSnapshotCapture') end |
.snapshot(driver, name, options = {}) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/percy.rb', line 88 def self.snapshot(driver, name, = {}) return unless percy_enabled? if @session_type == SESSION_TYPE_AUTOMATE raise StandardError, 'Invalid function call - percy_snapshot(). ' \ 'Please use percy_screenshot() function while using Percy with Automate. ' \ 'For more information on usage of percy_screenshot(), ' \ 'refer https://www.browserstack.com/docs/percy/integrate/functional-and-visual' end begin percy_dom_script = fetch_percy_dom driver.execute_script(percy_dom_script) # Merge .percy.yml config options with snapshot options (snapshot options take priority) = @cli_config&.dig('snapshot') || {} # Config keys are strings (JSON parse); per-call options use symbols, as # do downstream consumers (responsive_snapshot_capture?, capture_responsive_dom). # Deep-symbolize both sides so nested keys are consistent, then deep-merge # so nested Hashes merge recursively (per-call wins at leaves; arrays/scalars # replace) instead of a shallow top-level overwrite dropping config siblings. = (deep_symbolize(), deep_symbolize()) dom_snapshot = if responsive_snapshot_capture?() capture_responsive_dom(driver, , percy_dom_script: percy_dom_script) else get_serialized_dom(driver, , percy_dom_script: percy_dom_script) end # Strip `readiness` before POSTing -- SDK-local config that the CLI # already has via healthcheck. = .reject { |k, _| k.to_s == 'readiness' } response = fetch('percy/snapshot', name: name, url: driver.current_url, dom_snapshot: dom_snapshot, client_info: CLIENT_INFO, environment_info: ENV_INFO, **,) body = JSON.parse(response.body) unless body['success'] raise StandardError, body['error'] end body['data'] rescue StandardError => e log("Could not take DOM snapshot '#{name}'") log(e, 'debug') end end |
.unsupported_iframe_src?(src) ⇒ Boolean
257 258 259 260 |
# File 'lib/percy.rb', line 257 def self.unsupported_iframe_src?(src) src.nil? || src.empty? || src == 'about:blank' || src.start_with?('javascript:') || src.start_with?('data:') || src.start_with?('vbscript:') end |
.wait_for_ready(driver, options) ⇒ Object
Readiness gate: runs PercyDOM.waitForReady via execute_async_script BEFORE serialize. Graceful on old CLIs that lack the method. Returns readiness diagnostics (or nil) for attachment to domSnapshot.
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/percy.rb', line 165 def self.wait_for_ready(driver, ) readiness_config = resolve_readiness_config() return nil if readiness_config['preset'] == 'disabled' # Match the driver's async-script timeout to readiness.timeoutMs so a # higher user-configured timeout isn't silently capped by Selenium's # default (~30s) firing ScriptTimeoutException before the in-page # Promise resolves. timeout_ms = readiness_config['timeoutMs'] previous_timeout = nil if timeout_ms.is_a?(Numeric) && timeout_ms > 0 begin previous_timeout = driver.manage.timeouts.script_timeout driver.manage.timeouts.script_timeout = (timeout_ms / 1000.0) + 2 rescue StandardError previous_timeout = nil # best-effort; older Selenium / unsupported end end begin script = <<~JS var cfg = #{readiness_config.to_json}; var done = arguments[arguments.length - 1]; try { if (typeof PercyDOM !== 'undefined' && typeof PercyDOM.waitForReady === 'function') { PercyDOM.waitForReady(cfg).then(function(r){ done(r); }).catch(function(){ done(); }); } else { done(); } } catch (e) { done(); } JS driver.execute_async_script(script) rescue StandardError => e log("waitForReady failed, proceeding to serialize: #{e}", 'debug') nil ensure if previous_timeout begin driver.manage.timeouts.script_timeout = previous_timeout rescue StandardError # best-effort end end end end |