From 8a078462470c32e2821993ec82de92b8df646575 Mon Sep 17 00:00:00 2001 From: Shivanshu07 Date: Mon, 20 Apr 2026 11:26:48 +0530 Subject: [PATCH] feat: PER-7348 add waitForReady() call before serialize() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the readiness gate from percy/cli#2184. New WaitForReady() internal helper runs PercyDOM.waitForReady via ExecuteAsyncScript (callback signal) BEFORE the existing PercyDOM.serialize ExecuteScript inside getSerializedDom. Diagnostics are attached to domSnapshot as readiness_diagnostics. Serialize is unchanged. Config precedence: options["readiness"] > cliConfig.snapshot.readiness > empty (CLI applies balanced default). Backward compat via in-browser typeof guard. Disabled preset short-circuits. Graceful on any exception. Tests: two integration-style Facts exercise readiness-enabled and readiness-disabled paths via the public Snapshot API. dotnet build Percy/Percy.csproj → 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- Percy.Test/Percy.Test.cs | 40 +++++++++++++++++++++++++++ Percy/Percy.cs | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/Percy.Test/Percy.Test.cs b/Percy.Test/Percy.Test.cs index afe838e..ddbbf0f 100644 --- a/Percy.Test/Percy.Test.cs +++ b/Percy.Test/Percy.Test.cs @@ -338,6 +338,46 @@ public void PostSnapshotThrowExceptionWithAutomate() } Percy.Enabled = oldEnabledFn; } + + // --- Readiness gate (PER-7348) -------------------------------------- + + [Fact] + public void PostsSnapshotWithReadinessEnabled() + { + // preset=balanced → SDK calls PercyDOM.waitForReady via ExecuteAsyncScript + // before serialize. Snapshot still posts normally regardless of whether + // the connected CLI exposes waitForReady (typeof guard in the script). + Percy.Snapshot(driver, "readiness-balanced", new { + readiness = new { preset = "balanced" } + }); + + JsonElement data = Request("/test/logs"); + List logs = new List(); + foreach (JsonElement log in data.GetProperty("logs").EnumerateArray()) + { + string? msg = log.GetProperty("message").GetString(); + if (msg != null) logs.Add(msg); + } + Assert.Contains("Received snapshot: readiness-balanced", logs); + } + + [Fact] + public void PostsSnapshotWithReadinessDisabled() + { + // preset=disabled → SDK skips the ExecuteAsyncScript. Snapshot still posts. + Percy.Snapshot(driver, "readiness-disabled", new { + readiness = new { preset = "disabled" } + }); + + JsonElement data = Request("/test/logs"); + List logs = new List(); + foreach (JsonElement log in data.GetProperty("logs").EnumerateArray()) + { + string? msg = log.GetProperty("message").GetString(); + if (msg != null) logs.Add(msg); + } + Assert.Contains("Received snapshot: readiness-disabled", logs); + } } public class RegionTests { diff --git a/Percy/Percy.cs b/Percy/Percy.cs index 98f1731..0f3e38d 100644 --- a/Percy/Percy.cs +++ b/Percy/Percy.cs @@ -392,16 +392,75 @@ private static string GetOrigin(string url) }; } + // Readiness gate (PER-7348): runs PercyDOM.waitForReady via + // ExecuteAsyncScript (callback signal) BEFORE serialize. Graceful on + // old CLIs that lack waitForReady. Returns diagnostics for attachment. + internal static object? WaitForReady(WebDriver driver, Dictionary? options) + { + string readinessJson = "{}"; + if (options != null && options.TryGetValue("readiness", out var perSnapshot) && perSnapshot != null) + { + readinessJson = JsonSerializer.Serialize(perSnapshot); + } + else if (cliConfig is JsonElement cfg && + cfg.ValueKind == JsonValueKind.Object && + cfg.TryGetProperty("snapshot", out JsonElement snap) && + snap.ValueKind == JsonValueKind.Object && + snap.TryGetProperty("readiness", out JsonElement rd) && + rd.ValueKind == JsonValueKind.Object) + { + readinessJson = rd.GetRawText(); + } + + try + { + using JsonDocument doc = JsonDocument.Parse(readinessJson); + if (doc.RootElement.ValueKind == JsonValueKind.Object && + doc.RootElement.TryGetProperty("preset", out JsonElement presetEl) && + presetEl.ValueKind == JsonValueKind.String && + presetEl.GetString() == "disabled") + { + return null; + } + } + catch { /* fall through */ } + + string script = + "var cfg = " + readinessJson + ";" + + "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(); }"; + try + { + return driver.ExecuteAsyncScript(script); + } + catch (Exception e) + { + Log($"waitForReady failed, proceeding to serialize: {e.Message}", "debug"); + return null; + } + } + private static dynamic getSerializedDom( WebDriver driver, object cookies, Dictionary? options, string? domJs = null) { + // Readiness gate before serialize (PER-7348). Graceful on old CLI. + object? readinessDiagnostics = WaitForReady(driver, options); + var opts = JsonSerializer.Serialize(options); string script = $"return PercyDOM.serialize({opts})"; var domSnapshot = (Dictionary)driver.ExecuteScript(script); domSnapshot["cookies"] = cookies; + if (readinessDiagnostics != null) + { + domSnapshot["readiness_diagnostics"] = readinessDiagnostics; + } // Process CORS iframes when DOM script is available if (!string.IsNullOrEmpty(domJs))