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
40 changes: 40 additions & 0 deletions Percy.Test/Percy.Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> logs = new List<string>();
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<string> logs = new List<string>();
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
{
Expand Down
59 changes: 59 additions & 0 deletions Percy/Percy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object>? 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<string, object>? 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<string, object>)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))
Expand Down
Loading