Skip to content
Draft
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
36 changes: 36 additions & 0 deletions lib/percy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,44 @@ def self.get_browser_instance(driver)
driver.manage
end

# Readiness gate (PER-7348): 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.
#
# Config precedence: options[:readiness] / options['readiness'] >
# @cli_config.snapshot.readiness > {} (CLI applies balanced default).
# preset='disabled' skips the script call entirely.
def self.wait_for_ready(driver, options)
readiness_config = options[:readiness] || options['readiness']
if readiness_config.nil?
readiness_config = @cli_config&.dig('snapshot', 'readiness') || {}
end
return nil if readiness_config.is_a?(Hash) && readiness_config['preset'] == 'disabled'
return nil if readiness_config.is_a?(Hash) && readiness_config[:preset] == 'disabled'
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
end
end

def self.get_serialized_dom(driver, options, percy_dom_script: nil)
# Readiness gate before serialize (PER-7348). Graceful on old CLI.
readiness_diagnostics = wait_for_ready(driver, options)
dom_snapshot = driver.execute_script("return PercyDOM.serialize(#{options.to_json})")
if readiness_diagnostics && 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') : []
Expand Down
53 changes: 53 additions & 0 deletions spec/lib/percy/percy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,59 @@
expect(dom['corsIframes'].length).to eq(1)
expect(dom['corsIframes'][0]['frameUrl']).to eq('https://other.example.com/page')
end

# --- Readiness gate (PER-7348) --------------------------------------

it 'runs waitForReady before serialize and attaches diagnostics' do
allow(driver).to receive(:execute_async_script).and_return(
'ok' => true, 'timed_out' => false
)
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
allow(driver).to receive(:find_elements).and_return([])

dom = Percy.get_serialized_dom(driver, {})
expect(driver).to have_received(:execute_async_script) do |script|
expect(script).to include('waitForReady')
expect(script).to include("typeof PercyDOM.waitForReady === 'function'")
end
expect(dom['readiness_diagnostics']).to eq('ok' => true, 'timed_out' => false)
end

it 'embeds per-snapshot readiness config in the script' do
allow(driver).to receive(:execute_async_script).and_return(nil)
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
allow(driver).to receive(:find_elements).and_return([])

Percy.get_serialized_dom(driver, { readiness: { preset: 'strict', stabilityWindowMs: 500 } })
expect(driver).to have_received(:execute_async_script) do |script|
expect(script).to include('"preset":"strict"')
expect(script).to include('"stabilityWindowMs":500')
end
end

it 'skips execute_async_script when preset is disabled' do
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
allow(driver).to receive(:find_elements).and_return([])
expect(driver).to_not receive(:execute_async_script)

dom = Percy.get_serialized_dom(driver, { readiness: { preset: 'disabled' } })
expect(dom).to_not have_key('readiness_diagnostics')
expect(dom['html']).to eq('<html/>')
end

it 'still serializes when execute_async_script raises' do
allow(driver).to receive(:execute_async_script).and_raise(StandardError, 'readiness boom')
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
allow(driver).to receive(:find_elements).and_return([])

dom = Percy.get_serialized_dom(driver, {})
expect(dom).to_not have_key('readiness_diagnostics')
expect(dom['html']).to eq('<html/>')
end
end

describe '.change_window_dimension_and_wait' do
Expand Down
Loading