Skip to content

Commit 849436e

Browse files
Shivanshu-07claude
andcommitted
feat: PER-7348 add waitForReady() call before serialize()
Adds the readiness gate from percy/cli#2184. New wait_for_ready(page, options) helper runs PercyDOM.waitForReady via evaluate_async_script (callback signal) before the existing PercyDOM.serialize evaluate_script inside percy_snapshot. The result is attached to the dom_snapshot as 'readiness_diagnostics'. serialize is unchanged. Config: options[:readiness] / options['readiness'] > {} (CLI applies balanced default). Backward compat via in-browser typeof PercyDOM.waitForReady === 'function' guard. preset='disabled' skips evaluate_async_script. Graceful on any StandardError. Tests (RSpec): happy path verifies readiness_diagnostics propagates into the POST body via a mock PercyDOM.waitForReady; disabled preset verifies evaluate_async_script is NOT called. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cc62b05 commit 849436e

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

lib/percy/capybara.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,20 @@ def percy_snapshot(name, options = {})
2222

2323
begin
2424
page.evaluate_script(fetch_percy_dom)
25+
26+
# Readiness gate — runs before serialize when CLI supports it (PER-7348).
27+
# Uses evaluate_async_script with a callback signal so the SDK can block
28+
# on PercyDOM.waitForReady. In-browser typeof guard makes this a no-op on
29+
# older CLIs that lack waitForReady.
30+
readiness_diagnostics = wait_for_ready(page, options)
31+
2532
dom_snapshot = page
2633
.evaluate_script("(function() { return PercyDOM.serialize(#{options.to_json}) })()")
2734

35+
if readiness_diagnostics && dom_snapshot.is_a?(Hash)
36+
dom_snapshot['readiness_diagnostics'] = readiness_diagnostics
37+
end
38+
2839
response = fetch('percy/snapshot',
2940
name: name,
3041
url: page.current_url,
@@ -85,6 +96,33 @@ def percy_snapshot(name, options = {})
8596
@percy_dom = response.body
8697
end
8798

99+
# Readiness gate (PER-7348): runs PercyDOM.waitForReady before serialize.
100+
#
101+
# Returns diagnostics to attach to the domSnapshot, or nil.
102+
# Config precedence: options[:readiness] / options['readiness'] > {} (the
103+
# CLI applies its balanced preset default when passed {}). preset='disabled'
104+
# skips the script entirely. Any StandardError is caught at debug level.
105+
private def wait_for_ready(page, options)
106+
readiness_config = options[:readiness] || options['readiness'] || {}
107+
return nil if readiness_config.is_a?(Hash) && (
108+
readiness_config[:preset] == 'disabled' || readiness_config['preset'] == 'disabled'
109+
)
110+
begin
111+
page.evaluate_async_script(<<~JS)
112+
var cfg = #{readiness_config.to_json};
113+
var done = arguments[arguments.length - 1];
114+
try {
115+
if (typeof PercyDOM !== 'undefined' && typeof PercyDOM.waitForReady === 'function') {
116+
PercyDOM.waitForReady(cfg).then(function(r){ done(r); }).catch(function(){ done(); });
117+
} else { done(); }
118+
} catch (e) { done(); }
119+
JS
120+
rescue StandardError => e
121+
if PERCY_DEBUG then log("waitForReady failed, proceeding to serialize: #{e}") end
122+
nil
123+
end
124+
end
125+
88126
private def log(msg)
89127
puts "#{PERCY_LABEL} #{msg}"
90128
end

spec/lib/percy/percy_capybara_spec.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,52 @@
102102
).once
103103
expect(page).to have_current_path('/index.html')
104104
end
105+
106+
# --- Readiness gate (PER-7348) ----------------------------------------
107+
108+
it 'calls evaluate_async_script with waitForReady before serialize' do
109+
stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
110+
.to_return(status: 200, body: '', headers: {'x-percy-core-version': '1.0.0'})
111+
stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/dom.js")
112+
.to_return(
113+
status: 200,
114+
body: 'window.PercyDOM = { serialize: () => ({html: "<html></html>"}), ' \
115+
'waitForReady: (cfg) => Promise.resolve({ok: true}) };',
116+
headers: {},
117+
)
118+
stub_request(:post, 'http://localhost:5338/percy/snapshot')
119+
.to_return(status: 200, body: '{"success": "true"}', headers: {})
120+
121+
visit 'index.html'
122+
page.percy_snapshot('readiness-balanced')
123+
124+
# The snapshot POST body should include readiness_diagnostics from the mock
125+
expect(WebMock).to have_requested(
126+
:post, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/snapshot"
127+
).with { |req|
128+
body = JSON.parse(req.body)
129+
body.dig('dom_snapshot', 'readiness_diagnostics') == {'ok' => true}
130+
}.once
131+
end
132+
133+
it 'skips evaluate_async_script when preset is disabled' do
134+
stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
135+
.to_return(status: 200, body: '', headers: {'x-percy-core-version': '1.0.0'})
136+
stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/dom.js")
137+
.to_return(
138+
status: 200,
139+
body: 'window.PercyDOM = { serialize: () => ({html: "<html></html>"}) };',
140+
headers: {},
141+
)
142+
stub_request(:post, 'http://localhost:5338/percy/snapshot')
143+
.to_return(status: 200, body: '{"success": "true"}', headers: {})
144+
145+
# Spy on evaluate_async_script — it must NOT be called when preset=disabled
146+
expect(page).to_not receive(:evaluate_async_script)
147+
148+
visit 'index.html'
149+
page.percy_snapshot('readiness-disabled', readiness: { preset: 'disabled' })
150+
end
105151
end
106152
end
107153

0 commit comments

Comments
 (0)