Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions lib/cache.rb
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
class Cache
CACHE = {}
CACHE_TIMEOUT = 5 * 60 # 300 seconds
TIMEOUT_KEY = 'last_access_time'
CACHE = {} # rubocop:disable Style/MutableConstant
CACHE_TIMEOUT = 5 * 60 # 300 seconds
TIMEOUT_KEY = 'last_access_time'.freeze

# Caching Keys
CAPABILITIES = 'capabilities'
COMMAND_EXECUTOR_URL = 'command_executor_url'
# Caching Keys
CAPABILITIES = 'capabilities'.freeze
COMMAND_EXECUTOR_URL = 'command_executor_url'.freeze

def self.check_types(session_id, property)
raise TypeError, 'Argument session_id should be string' unless session_id.is_a?(String)
raise TypeError, 'Argument property should be string' unless property.is_a?(String)
end
def self.check_types(session_id, property)
raise TypeError, 'Argument session_id should be string' unless session_id.is_a?(String)
raise TypeError, 'Argument property should be string' unless property.is_a?(String)
end

def self.set_cache(session_id, property, value)
check_types(session_id, property)
session = CACHE[session_id] || {}
session[TIMEOUT_KEY] = Time.now.to_f
session[property] = value
CACHE[session_id] = session
end
def self.set_cache(session_id, property, value)
check_types(session_id, property)
session = CACHE[session_id] || {}
session[TIMEOUT_KEY] = Time.now.to_f
session[property] = value
CACHE[session_id] = session
end

def self.get_cache(session_id, property)
cleanup_cache
check_types(session_id, property)
session = CACHE[session_id] || {}
session[property]
end
def self.get_cache(session_id, property)
cleanup_cache
check_types(session_id, property)
session = CACHE[session_id] || {}
session[property]
end

def self.cleanup_cache
now = Time.now.to_f
CACHE.delete_if do |_, session|
timestamp = session[TIMEOUT_KEY]
timestamp && (now - timestamp >= CACHE_TIMEOUT)
end
def self.cleanup_cache
now = Time.now.to_f
CACHE.delete_if do |_, session|
timestamp = session[TIMEOUT_KEY]
timestamp && (now - timestamp >= CACHE_TIMEOUT)
end
end

def self.clear_cache!
CACHE.clear
end
def self.clear_cache!
CACHE.clear
end
end
83 changes: 42 additions & 41 deletions lib/driver_metadata.rb
Original file line number Diff line number Diff line change
@@ -1,57 +1,58 @@
require_relative 'cache'

class DriverMetaData
def initialize(driver)
@driver = driver
def initialize(driver)
@driver = driver
end

def session_id
@driver.session_id
end

def command_executor_url
cached = Cache.get_cache(session_id, Cache::COMMAND_EXECUTOR_URL)
return cached unless cached.nil?

url = nil

begin
raw = @driver.send(:bridge).http.send(:server_url)
url = raw.to_s unless raw.nil?
rescue StandardError => e
Percy.log("Could not get command_executor_url via bridge.http.server_url: #{e}",
'debug') if defined?(Percy)
end

def session_id
@driver.session_id
end

def command_executor_url
cached = Cache.get_cache(session_id, Cache::COMMAND_EXECUTOR_URL)
return cached unless cached.nil?

url = nil

if url.nil? || url.empty?
begin
raw = @driver.send(:bridge).http.send(:server_url)
raw = @driver.send(:bridge).http.instance_variable_get(:@server_url)
url = raw.to_s unless raw.nil?
rescue StandardError => e
Percy.log("Could not get command_executor_url via bridge.http.server_url: #{e}", 'debug') if defined?(Percy)
end

if url.nil? || url.empty?
begin
raw = @driver.send(:bridge).http.instance_variable_get(:@server_url)
url = raw.to_s unless raw.nil?
rescue StandardError => e
Percy.log("Could not get @server_url instance variable: #{e}", 'debug') if defined?(Percy)
end
Percy.log("Could not get @server_url instance variable: #{e}", 'debug') if defined?(Percy)
end

url ||= ''
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
url
end

def capabilities
cached = Cache.get_cache(session_id, Cache::CAPABILITIES)
return cached unless cached.nil?
url ||= ''
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
url
end

def capabilities
cached = Cache.get_cache(session_id, Cache::CAPABILITIES)
return cached unless cached.nil?

caps = begin
@driver.capabilities.as_json
caps = nil
begin
caps = @driver.capabilities.as_json
rescue StandardError
begin
caps = @driver.capabilities.to_h
rescue StandardError
begin
@driver.capabilities.to_h
rescue StandardError
{}
end
caps = {}
end

Cache.set_cache(session_id, Cache::CAPABILITIES, caps)
caps
end
end

Cache.set_cache(session_id, Cache::CAPABILITIES, caps)
caps
end
end
63 changes: 40 additions & 23 deletions lib/percy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ module Percy
PERCY_SERVER_ADDRESS = ENV['PERCY_SERVER_ADDRESS'] || 'http://localhost:5338'
LABEL = "[\u001b[35m" + (PERCY_DEBUG ? 'percy:ruby' : 'percy') + "\u001b[39m]"
RESONSIVE_CAPTURE_SLEEP_TIME = ENV['RESONSIVE_CAPTURE_SLEEP_TIME']
PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE = (ENV['PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE'] || 'false').downcase
PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT = (ENV['PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT'] || 'false').downcase

PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE =
(ENV['PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE'] || 'false').downcase
PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT =
(ENV['PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT'] || 'false').downcase

def self.create_region(
bounding_box: nil, element_xpath: nil, element_css: nil, padding: nil,
algorithm: 'ignore', diff_sensitivity: nil, image_ignore_threshold: nil,
Expand Down Expand Up @@ -145,7 +147,7 @@ def self.unsupported_iframe_src?(src)
def self.get_origin(url)
uri = URI.parse(url)
netloc = uri.host.to_s
default_ports = { 'http' => 80, 'https' => 443 }
default_ports = {'http' => 80, 'https' => 443}
netloc += ":#{uri.port}" if uri.port && uri.port != default_ports[uri.scheme]
"#{uri.scheme}://#{netloc}"
end
Expand All @@ -159,7 +161,9 @@ def self.process_frame(driver, frame_element, options, percy_dom_script)
begin
driver.execute_script(percy_dom_script)
iframe_options = options.merge('enableJavaScript' => true)
iframe_snapshot = driver.execute_script("return PercyDOM.serialize(#{iframe_options.to_json})")
iframe_snapshot = driver.execute_script(
"return PercyDOM.serialize(#{iframe_options.to_json})"
)
rescue StandardError => e
log("Failed to process cross-origin frame #{frame_url}: #{e}", 'debug')
ensure
Expand All @@ -179,26 +183,29 @@ def self.process_frame(driver, frame_element, options, percy_dom_script)
end

{
'iframeData' => { 'percyElementId' => percy_element_id },
'iframeData' => {'percyElementId' => percy_element_id},
'iframeSnapshot' => iframe_snapshot,
'frameUrl' => frame_url
'frameUrl' => frame_url,
}
end

def self.get_responsive_widths(widths = [])
begin
widths_list = widths.is_a?(Array) ? widths : []
query_param = widths_list.any? ? "?widths=#{widths_list.join(',')}": ''
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)
raise StandardError, 'Update Percy CLI to the latest version to use responsiveSnapshotCapture'
raise StandardError,
'Update Percy CLI to the latest version to use responsiveSnapshotCapture'
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'
raise StandardError,
'Update Percy CLI to the latest version to use responsiveSnapshotCapture'
end
end

Expand All @@ -210,25 +217,24 @@ def self.change_window_dimension_and_wait(driver, width, height, resize_count)
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)
sleep(0.5)
sleep(0.5)
# 3. FORCE the event to fire so PercyDOM and page listeners react
driver.execute_script("window.dispatchEvent(new Event('resize'));")
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)
sleep(0.5)
driver.execute_script("window.dispatchEvent(new Event('resize'));")
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 }")
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')
Expand All @@ -240,8 +246,14 @@ def self.capture_responsive_dom(driver, options, percy_dom_script: nil)
log(widths.to_s, 'debug')
dom_snapshots = []
window_size = get_browser_instance(driver).window.size
initial_viewport = driver.execute_script("return { w: window.innerWidth, h: window.innerHeight }")
log("Initial Window Size: #{window_size.width}x#{window_size.height} (Viewport: #{initial_viewport['w']}x#{initial_viewport['h']})", 'debug')
initial_viewport = driver.execute_script(
'return { w: window.innerWidth, h: window.innerHeight }'
)
log(
"Initial Window Size: #{window_size.width}x#{window_size.height} " \
"(Viewport: #{initial_viewport['w']}x#{initial_viewport['h']})",
'debug'
)
current_width = window_size.width
current_height = window_size.height
last_window_width = current_width
Expand All @@ -256,7 +268,9 @@ def self.capture_responsive_dom(driver, options, percy_dom_script: nil)
log("current minheight #{min_height}", 'debug')
if min_height
begin
target_height = driver.execute_script("return window.outerHeight - window.innerHeight + #{min_height}")
target_height = driver.execute_script(
"return window.outerHeight - window.innerHeight + #{min_height}"
)
log("Calculated height for responsive capture using minHeight: #{target_height}", 'debug')
rescue StandardError => e
log("Failed to calculate responsive target height: #{e}", 'debug')
Expand Down Expand Up @@ -412,11 +426,14 @@ def self.percy_screenshot(driver, name, options = {})
options[:ignore_region_selenium_elements] = options.delete(:ignoreRegionSeleniumElements)
end
if options.key?(:considerRegionSeleniumElements)
options[:consider_region_selenium_elements] = options.delete(:considerRegionSeleniumElements)
options[:consider_region_selenium_elements] =
options.delete(:considerRegionSeleniumElements)
end

ignore_region_elements = get_element_ids(options.delete(:ignore_region_selenium_elements) || [])
consider_region_elements = get_element_ids(options.delete(:consider_region_selenium_elements) || [])
ignore_region_elements =
get_element_ids(options.delete(:ignore_region_selenium_elements) || [])
consider_region_elements =
get_element_ids(options.delete(:consider_region_selenium_elements) || [])

options[:ignore_region_elements] = ignore_region_elements
options[:consider_region_elements] = consider_region_elements
Expand All @@ -428,7 +445,7 @@ def self.percy_screenshot(driver, name, options = {})
commandExecutorUrl: metadata.command_executor_url,
capabilities: metadata.capabilities,
snapshotName: name,
options: options)
options: options,)

body = JSON.parse(response.body)
unless body['success']
Expand Down
10 changes: 5 additions & 5 deletions spec/lib/percy/cache_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
RSpec.describe Cache do
let(:session_id) { 'session_id_123' }
let(:url) { 'https://example-hub:4444/wd/hub' }
let(:caps) { { 'browser' => 'chrome', 'platform' => 'windows', 'browserVersion' => '115.0.1' } }
let(:caps) { {'browser' => 'chrome', 'platform' => 'windows', 'browserVersion' => '115.0.1'} }

before(:each) { Cache.clear_cache! }

Expand All @@ -19,7 +19,7 @@
end

it 'does not raise when both arguments are strings' do
expect { Cache.check_types(session_id, Cache::COMMAND_EXECUTOR_URL) }.not_to raise_error
expect { Cache.check_types(session_id, Cache::COMMAND_EXECUTOR_URL) }.to_not raise_error
end
end

Expand Down Expand Up @@ -54,7 +54,7 @@
end

describe '.get_cache' do
before do
before(:each) do
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
Cache.set_cache(session_id, Cache::CAPABILITIES, caps)
end
Expand Down Expand Up @@ -96,7 +96,7 @@
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
Cache::CACHE[session_id][Cache::TIMEOUT_KEY] = Time.now.to_f - (Cache::CACHE_TIMEOUT + 1)
Cache.cleanup_cache
expect(Cache::CACHE).not_to have_key(session_id)
expect(Cache::CACHE).to_not have_key(session_id)
end

it 'keeps entries that have not exceeded the cache timeout' do
Expand All @@ -112,7 +112,7 @@
Cache::CACHE[expired_session][Cache::TIMEOUT_KEY] = Time.now.to_f - (Cache::CACHE_TIMEOUT + 1)
Cache.cleanup_cache
expect(Cache::CACHE).to have_key(session_id)
expect(Cache::CACHE).not_to have_key(expired_session)
expect(Cache::CACHE).to_not have_key(expired_session)
end
end

Expand Down
Loading
Loading