Skip to content

Commit

Permalink
Added support for responsive snapshot capture
Browse files Browse the repository at this point in the history
  • Loading branch information
chinmay-browserstack committed Oct 1, 2024
1 parent d0bac3d commit 31696eb
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 23 deletions.
118 changes: 112 additions & 6 deletions lib/percy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ module Percy
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]"
RESONSIVE_CAPTURE_SLEEP_TIME = ENV['RESONSIVE_CAPTURE_SLEEP_TIME']

# Take a DOM snapshot and post it to the snapshot endpoint
def self.snapshot(driver, name, options = {})
return unless percy_enabled?

begin
driver.execute_script(fetch_percy_dom)
dom_snapshot = driver.execute_script("return PercyDOM.serialize(#{options.to_json})")
dom_snapshot = nil
if is_responsive_snapshot_capture?(options)
dom_snapshot = capture_responsive_dom(driver, options)
else
dom_snapshot = get_serialized_dom(driver, options)
end

response = fetch('percy/snapshot',
name: name,
Expand All @@ -36,9 +42,94 @@ def self.snapshot(driver, name, options = {})
body['data']
rescue StandardError => e
log("Could not take DOM snapshot '#{name}'")
log(e, "debug")
end
end

if PERCY_DEBUG then log(e) end
def self.get_browser_instance(driver)
if driver.is_a?(Capybara::Session)
return driver.driver.browser.manage
end
return driver.manage
end

def self.get_serialized_dom(driver, options)
dom_snapshot = driver.execute_script("return PercyDOM.serialize(#{options.to_json})")

dom_snapshot['cookies'] = get_browser_instance(driver).all_cookies
dom_snapshot
end

def self.get_widths_for_multi_dom(options)
user_passed_widths = options[:widths] || []

# Deep copy mobile widths otherwise it will get overridden
all_widths = @eligible_widths['mobile']&.dup || []
if user_passed_widths.any?
all_widths.concat(user_passed_widths)
else
all_widths.concat(@eligible_widths['config'] || [])
end

all_widths.uniq
end

def self.change_window_dimension_and_wait(driver, width, height, resize_count)
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)
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)
end

begin
wait = Selenium::WebDriver::Wait.new(timeout: 1)
wait.until { driver.execute_script("return window.resizeCount") == resize_count }
rescue Selenium::WebDriver::Error::TimeoutError
log("Timed out waiting for window resize event for width #{width}", 'debug')
end
end

def self.capture_responsive_dom(driver, options)
widths = get_widths_for_multi_dom(options)
dom_snapshots = []
window_size = get_browser_instance(driver).window.size
current_width, current_height = window_size.width, window_size.height
last_window_width = current_width
resize_count = 0
driver.execute_script("PercyDOM.waitForResize()")

widths.each do |width|
if last_window_width != width
resize_count += 1
change_window_dimension_and_wait(driver, width, current_height, resize_count)
last_window_width = width
end

sleep(RESONSIVE_CAPTURE_SLEEP_TIME.to_i) if defined?(RESONSIVE_CAPTURE_SLEEP_TIME)

dom_snapshot = get_serialized_dom(driver, options)
dom_snapshot['width'] = width
dom_snapshots << dom_snapshot
end

change_window_dimension_and_wait(driver, current_width, current_height, resize_count + 1)
dom_snapshots
end

def self.is_responsive_snapshot_capture?(options)
# Don't run responsive snapshot capture when defer uploads is enabled
return false if @cli_config && @cli_config.dig('percy', 'deferUploads')

options[:responsive_snapshot_capture] ||
options[:responsiveSnapshotCapture] ||
(@cli_config && @cli_config.dig('snapshot', 'responsiveSnapshotCapture'))
end

# Determine if the Percy server is running, caching the result so it is only checked once
Expand All @@ -64,12 +155,14 @@ def self.percy_enabled?
return false
end

response_body = JSON.parse(response.body)
@eligible_widths = response_body['widths']
@cli_config = response_body["config"]
@percy_enabled = true
true
rescue StandardError => e
log('Percy is not running, disabling snapshots')

if PERCY_DEBUG then log(e) end
log(e, 'debug')
@percy_enabled = false
false
end
Expand All @@ -83,8 +176,19 @@ def self.fetch_percy_dom
@percy_dom = response.body
end

def self.log(msg)
puts "#{LABEL} #{msg}"
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

# Make an HTTP request (GET,POST) using Ruby's Net::HTTP. If `data` is present,
Expand Down Expand Up @@ -112,5 +216,7 @@ def self.fetch(url, data = nil)
def self._clear_cache!
@percy_dom = nil
@percy_enabled = nil
@eligible_widths = nil
@cli_config = nil
end
end
15 changes: 7 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{
"private": true,
"scripts": {
"test": "percy exec --testing -- bundle exec rspec"
},
"devDependencies": {
"@percy/cli": "^1.28.0"
}
"private": true,
"scripts": {
"test": "percy exec --testing -- bundle exec rspec"
},
"devDependencies": {
"@percy/cli": "^1.29.5-beta.0"
}
}
55 changes: 46 additions & 9 deletions spec/lib/percy/percy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
RSpec.describe Percy, type: :feature do
before(:each) do
WebMock.disable_net_connect!(allow: '127.0.0.1', disallow: 'localhost')
stub_request(:post, "http://localhost:5338/percy/log").to_raise(StandardError)
Percy._clear_cache!
end

Expand Down Expand Up @@ -59,12 +60,12 @@

it 'logs an error when sending a snapshot fails' do
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
.to_return(status: 200, body: '', headers: {'x-percy-core-version': '1.0.0'})
.to_return(status: 200, body: '{"success": "true" }', headers: {'x-percy-core-version': '1.0.0'})

stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/dom.js")
.to_return(
status: 200,
body: 'window.PercyDOM = { serialize: () => document.documentElement.outerHTML };',
body: 'window.PercyDOM = { serialize: () => { return { html: document.documentElement.outerHTML, cookies: "" } }};',
headers: {},
)

Expand All @@ -77,12 +78,12 @@

it 'sends snapshots to the local server' do
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
.to_return(status: 200, body: '', headers: {'x-percy-core-version': '1.0.0'})
.to_return(status: 200, body: '{"success": "true" }', headers: {'x-percy-core-version': '1.0.0'})

stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/dom.js")
.to_return(
status: 200,
body: 'window.PercyDOM = { serialize: () => document.documentElement.outerHTML };',
body: 'window.PercyDOM = { serialize: () => { return { html: document.documentElement.outerHTML, cookies: "" } }};',
headers: {},
)

Expand All @@ -98,7 +99,7 @@
name: 'Name',
url: 'http://127.0.0.1:3003/index.html',
dom_snapshot:
"<html><head><title>I am a page</title></head><body>Snapshot me\n</body></html>",
{ "cookies": [], "html": "<html><head><title>I am a page</title></head><body>Snapshot me\n</body></html>" },
client_info: "percy-selenium-ruby/#{Percy::VERSION}",
environment_info: "selenium/#{Selenium::WebDriver::VERSION} ruby/#{RUBY_VERSION}",
widths: [944],
Expand All @@ -108,14 +109,50 @@
expect(data).to eq(nil)
end

it 'sends multiple dom snapshots to the local server' do
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
.to_return(status: 200, body: '{"success": "true", "widths": { "mobile": [390], "config": [765, 1280]} }', headers: {'x-percy-core-version': '1.0.0', 'config': {}, 'widths': { 'mobile': [375], 'config': [765, 1280]}})

stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/dom.js")
.to_return(
status: 200,
body: 'window.PercyDOM = { serialize: () => { return { html: document.documentElement.outerHTML, cookies: "" } }, waitForResize: () => { if(!window.resizeCount) { window.addEventListener(\'resize\', () => window.resizeCount++) } window.resizeCount = 0; }};',
headers: {},
)

stub_request(:post, 'http://localhost:5338/percy/snapshot')
.to_return(status: 200, body: '{"success": "true" }', headers: {})

visit 'index.html'
data = Percy.snapshot(page, 'Name', { responsive_snapshot_capture: true })

expect(WebMock).to have_requested(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/snapshot")
.with(
body: {
name: 'Name',
url: 'http://127.0.0.1:3003/index.html',
dom_snapshot: [
{ 'cookies': [], 'html': "<html><head><title>I am a page</title></head><body>Snapshot me\n</body></html>", 'width': 390 },
{ 'cookies': [], 'html': "<html><head><title>I am a page</title></head><body>Snapshot me\n</body></html>", 'width': 765 },
{ 'cookies': [], 'html': "<html><head><title>I am a page</title></head><body>Snapshot me\n</body></html>", 'width': 1280 }
],
client_info: "percy-selenium-ruby/#{Percy::VERSION}",
environment_info: "selenium/#{Selenium::WebDriver::VERSION} ruby/#{RUBY_VERSION}",
responsive_snapshot_capture: true
}.to_json,
).once

expect(data).to eq(nil)
end

it 'sends snapshots for sync' do
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
.to_return(status: 200, body: '', headers: {'x-percy-core-version': '1.0.0'})
.to_return(status: 200, body: '{"success": "true" }', headers: {'x-percy-core-version': '1.0.0'})

stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/dom.js")
.to_return(
status: 200,
body: 'window.PercyDOM = { serialize: () => document.documentElement.outerHTML };',
body: 'window.PercyDOM = { serialize: () => { return { html: document.documentElement.outerHTML, cookies: "" } }}',
headers: {},
)

Expand All @@ -131,10 +168,10 @@
name: 'Name',
url: 'http://127.0.0.1:3003/index.html',
dom_snapshot:
"<html><head><title>I am a page</title></head><body>Snapshot me\n</body></html>",
{ "cookies" => [], "html" => "<html><head><title>I am a page</title></head><body>Snapshot me\n</body></html>" },
client_info: "percy-selenium-ruby/#{Percy::VERSION}",
environment_info: "selenium/#{Selenium::WebDriver::VERSION} ruby/#{RUBY_VERSION}",
widths: [944],
sync: true,
}.to_json,
).once

Expand Down

0 comments on commit 31696eb

Please sign in to comment.