-
Notifications
You must be signed in to change notification settings - Fork 4
feat: PER-7348 add waitForReady() call before serialize() #218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,9 +191,54 @@ def _get_origin(url): | |
| parsed = urlparse(url) | ||
| return f"{parsed.scheme}://{parsed.netloc}" | ||
|
|
||
| def _wait_for_ready(driver, kwargs): | ||
| """Run readiness checks before serialize. PER-7348. | ||
|
|
||
| Sends PercyDOM.waitForReady via execute_async_script. The script checks | ||
| typeof PercyDOM.waitForReady in-browser so older CLI versions without the | ||
| method are a graceful no-op. Any failure is caught and logged at debug; | ||
| serialize still runs. | ||
|
|
||
| Returns readiness diagnostics dict (or None) so callers can attach it | ||
| to the domSnapshot for CLI-side logging. | ||
|
|
||
| Readiness config precedence: kwargs['readiness'] > cached | ||
| percy.config.snapshot.readiness > {} (CLI falls back to balanced default). | ||
| If preset is 'disabled', skip the async script call entirely. | ||
| """ | ||
| readiness_config = kwargs.get('readiness') | ||
| if readiness_config is None: | ||
| data = is_percy_enabled() | ||
| if isinstance(data, dict): | ||
| readiness_config = (data.get('config') or {}).get('snapshot', {}).get('readiness', {}) or {} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
| else: | ||
| readiness_config = {} | ||
| if isinstance(readiness_config, dict) and readiness_config.get('preset') == 'disabled': | ||
| return None | ||
| try: | ||
| diagnostics = driver.execute_async_script( | ||
| 'var config = ' + json.dumps(readiness_config) + ';' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two concerns on this
|
||
| 'var done = arguments[arguments.length - 1];' | ||
| 'try {' | ||
| " if (typeof PercyDOM !== 'undefined' && typeof PercyDOM.waitForReady === 'function') {" | ||
| ' PercyDOM.waitForReady(config).then(function(r){ done(r); }).catch(function(){ done(); });' | ||
| ' } else { done(); }' | ||
| '} catch(e) { done(); }' | ||
| ) | ||
| return diagnostics | ||
| except Exception as e: | ||
| log(f'waitForReady failed, proceeding to serialize: {e}', 'debug') | ||
| return None | ||
|
|
||
|
|
||
| def get_serialized_dom(driver, cookies, percy_dom_script=None, **kwargs): | ||
| # 0. Readiness gate before serialize (PER-7348). Graceful on old CLI. | ||
| readiness_diagnostics = _wait_for_ready(driver, kwargs) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Safer: |
||
| # 1. Serialize the main page first (this adds the data-percy-element-ids) | ||
| dom_snapshot = driver.execute_script(f'return PercyDOM.serialize({json.dumps(kwargs)})') | ||
| # Attach readiness diagnostics so the CLI can log timing and pass/fail | ||
| if readiness_diagnostics and isinstance(dom_snapshot, dict): | ||
| dom_snapshot['readiness_diagnostics'] = readiness_diagnostics | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the |
||
| # 2. Process CORS IFrames | ||
| try: | ||
| page_origin = _get_origin(driver.current_url) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -472,6 +472,72 @@ def test_raise_error_poa_token_with_snapshot(self): | |
| " For more information on usage of PercyScreenshot, refer https://www.browserstack.com"\ | ||
| "/docs/percy/integrate/functional-and-visual", str(context.exception)) | ||
|
|
||
| # --- Readiness gate (PER-7348) --------------------------------------- | ||
|
|
||
| def test_readiness_runs_before_serialize_by_default(self): | ||
| mock_healthcheck() | ||
| mock_snapshot() | ||
|
|
||
| with patch.object(self.driver, 'execute_async_script', wraps=self.driver.execute_async_script) as async_spy, \ | ||
| patch.object(self.driver, 'execute_script', wraps=self.driver.execute_script) as sync_spy: | ||
| percy_snapshot(self.driver, 'readiness-happy-path') | ||
|
|
||
| async_scripts = [c.args[0] for c in async_spy.call_args_list if c.args] | ||
| sync_scripts = [c.args[0] for c in sync_spy.call_args_list if c.args] | ||
| # Readiness call made at least once, and contains the typeof guard + waitForReady | ||
| self.assertTrue(any('waitForReady' in s and 'typeof PercyDOM' in s for s in async_scripts), | ||
| f'expected readiness script via execute_async_script, got: {async_scripts}') | ||
| # Serialize call made at least once via sync execute_script | ||
| self.assertTrue(any('PercyDOM.serialize' in s for s in sync_scripts), | ||
| f'expected serialize via execute_script, got: {sync_scripts}') | ||
|
|
||
| def test_readiness_uses_per_snapshot_config(self): | ||
| mock_healthcheck() | ||
| mock_snapshot() | ||
|
|
||
| readiness = {'preset': 'strict', 'stabilityWindowMs': 500} | ||
| with patch.object(self.driver, 'execute_async_script', wraps=self.driver.execute_async_script) as async_spy: | ||
| percy_snapshot(self.driver, 'readiness-config', readiness=readiness) | ||
|
|
||
| scripts = [c.args[0] for c in async_spy.call_args_list if c.args] | ||
| readiness_scripts = [s for s in scripts if 'waitForReady' in s] | ||
| self.assertTrue(readiness_scripts, 'readiness script should have been sent') | ||
| # JSON-serialized config embedded in the script | ||
| self.assertIn('"preset": "strict"', readiness_scripts[0]) | ||
| self.assertIn('"stabilityWindowMs": 500', readiness_scripts[0]) | ||
|
|
||
| def test_readiness_skipped_when_preset_disabled(self): | ||
| mock_healthcheck() | ||
| mock_snapshot() | ||
|
|
||
| with patch.object(self.driver, 'execute_async_script', wraps=self.driver.execute_async_script) as async_spy, \ | ||
| patch.object(self.driver, 'execute_script', wraps=self.driver.execute_script) as sync_spy: | ||
| percy_snapshot(self.driver, 'readiness-disabled', | ||
| readiness={'preset': 'disabled'}) | ||
|
|
||
| async_scripts = [c.args[0] for c in async_spy.call_args_list if c.args] | ||
| sync_scripts = [c.args[0] for c in sync_spy.call_args_list if c.args] | ||
| self.assertFalse(any('waitForReady' in s for s in async_scripts), | ||
| f'readiness script should NOT have been sent, got: {async_scripts}') | ||
| # Serialize still ran | ||
| self.assertTrue(any('PercyDOM.serialize' in s for s in sync_scripts)) | ||
|
|
||
| def test_snapshot_still_posts_when_readiness_raises(self): | ||
| mock_healthcheck() | ||
| mock_snapshot() | ||
|
|
||
| # Make the readiness call raise; serialize should still run and the | ||
| # snapshot POST should still be made. | ||
| def explode(*args, **kwargs): | ||
| raise RuntimeError('readiness boom') | ||
|
|
||
| with patch.object(self.driver, 'execute_async_script', side_effect=explode): | ||
| percy_snapshot(self.driver, 'readiness-boom') | ||
|
|
||
| # Snapshot endpoint was hit | ||
| paths = [req.path for req in httpretty.latest_requests()] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test validates the POST happens when |
||
| self.assertIn('/percy/snapshot', paths) | ||
|
|
||
| class TestPercyScreenshot(unittest.TestCase): | ||
| @classmethod | ||
| def setUpClass(cls): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant call.
percy_snapshot(the only production caller path) has already calledis_percy_enabled()and hasdata['config']in scope. Plumb the config through explicitly rather than relying on the lru_cache re-lookup — it makes the data flow clearer and avoids a surprise dependency on the cache for anyone who callsget_serialized_domdirectly (module-level, no leading underscore).