diff --git a/pom.xml b/pom.xml index 468d183..e563227 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,16 @@ playwright 1.44.0 + + + com.google.code.gson + gson + 2.10.1 + org.json json diff --git a/src/main/java/io/percy/playwright/Percy.java b/src/main/java/io/percy/playwright/Percy.java index 007a6cb..e4c4c4b 100644 --- a/src/main/java/io/percy/playwright/Percy.java +++ b/src/main/java/io/percy/playwright/Percy.java @@ -21,6 +21,9 @@ import com.microsoft.playwright.*; import com.microsoft.playwright.options.Cookie; import com.microsoft.playwright.options.ViewportSize; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; /** @@ -319,6 +322,10 @@ public JSONObject snapshot(String name, Map options) { String percyDomScript = fetchPercyDOM(); page.evaluate(percyDomScript); + // Expose closed shadow roots via CDP before serialization so + // PercyDOM.serialize() can access them through the WeakMap + exposeClosedShadowRoots(page); + List cookies = new ArrayList<>(); try { cookies = page.context().cookies(); @@ -831,6 +838,132 @@ private List> captureResponsiveDom( // Cross-origin iframe helpers // ------------------------------------------------------------------------- + /** + * Resolves the list of iframe-ignore CSS selectors for this snapshot. + * + *

Mirrors the JS SDK's {@code resolveIgnoreSelectors(options)}: per-snapshot + * {@code options.ignoreIframeSelectors} (falling back to {@code options.ignoreSelectors}) + * takes precedence; if absent, the global {@code cliConfig.snapshot.ignoreIframeSelectors} + * is used. The value is normalized to a {@code List} of non-empty strings. + * A single string is wrapped in a one-element list. Any other type (or a missing + * value) yields an empty list, which makes the filter a no-op.

+ */ + @SuppressWarnings("unchecked") + List resolveIgnoreSelectors(Map options) { + Object sel = null; + if (options != null) { + sel = options.get("ignoreIframeSelectors"); + if (sel == null) { sel = options.get("ignoreSelectors"); } + } + // Fall back to global config (cliConfig.snapshot.ignoreIframeSelectors) + if (sel == null && cliConfig != null && cliConfig.has("snapshot") + && !cliConfig.isNull("snapshot")) { + JSONObject snapshotConfig = cliConfig.optJSONObject("snapshot"); + if (snapshotConfig != null && snapshotConfig.has("ignoreIframeSelectors") + && !snapshotConfig.isNull("ignoreIframeSelectors")) { + Object configSel = snapshotConfig.get("ignoreIframeSelectors"); + if (configSel instanceof JSONArray) { + List out = new ArrayList<>(); + JSONArray arr = (JSONArray) configSel; + for (int i = 0; i < arr.length(); i++) { + Object v = arr.opt(i); + if (v instanceof String && !((String) v).isEmpty()) { out.add((String) v); } + } + return out; + } else if (configSel instanceof String) { + return Collections.singletonList((String) configSel); + } + return new ArrayList<>(); + } + } + + if (sel == null) { return new ArrayList<>(); } + if (sel instanceof List) { + List out = new ArrayList<>(); + for (Object v : (List) sel) { + if (v instanceof String && !((String) v).isEmpty()) { out.add((String) v); } + } + return out; + } + if (sel instanceof String) { + return Collections.singletonList((String) sel); + } + return new ArrayList<>(); + } + + /** + * Reads the {@code data-percy-ignore} attribute and ignore-selector match + * state of a cross-origin frame's {@code