Repository: microsoft/playwright-java (2025-07-24T09:23:24Z)

Issue:
[Bug]: Locator screenshot with mask always fails - serialization bug
### Version

1.52.0

### Steps to reproduce

Use this reproduction example, run the main method.

```
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import java.util.List;

public class LocatorScreenshotBugReproducer {

  public static void main(String[] args) {
    try (Playwright playwright = Playwright.create();
        Browser browser = playwright.chromium().launch()) {
      BrowserContext context = browser.newContext();
      Page page = context.newPage();
      page.navigate("https://www.playwright.dev");

      var heading = page.locator("h1");

      heading.waitFor();

      // this throws exception, only if a mask is specified
      heading.screenshot(
          new Locator.ScreenshotOptions()
              .setMask(List.of(heading.locator("span"))));

    } catch (Exception e) {
      e.printStackTrace();
      throw e;
    }
  }
}
```

### Expected behavior

Screenshot should be taken

### Actual behavior

The code fails with an exception and the following stack trace:

```
com.google.gson.JsonIOException: Failed making field 'java.lang.Throwable#detailMessage' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.
	at com.google.gson.internal.reflect.ReflectionHelper.makeAccessible(ReflectionHelper.java:38)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:286)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:130)
	at com.google.gson.Gson.getAdapter(Gson.java:556)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.createBoundField(ReflectiveTypeAdapterFactory.java:160)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:294)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:130)
	at com.google.gson.Gson.getAdapter(Gson.java:556)
	at com.google.gson.internal.bind.MapTypeAdapterFactory.create(MapTypeAdapterFactory.java:125)
	at com.google.gson.Gson.getAdapter(Gson.java:556)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.createBoundField(ReflectiveTypeAdapterFactory.java:160)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:294)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:130)
	at com.google.gson.Gson.getAdapter(Gson.java:556)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.createBoundField(ReflectiveTypeAdapterFactory.java:160)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:294)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:130)
	at com.google.gson.Gson.getAdapter(Gson.java:556)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.createBoundField(ReflectiveTypeAdapterFactory.java:160)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:294)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:130)
	at com.google.gson.Gson.getAdapter(Gson.java:556)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.createBoundField(ReflectiveTypeAdapterFactory.java:160)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:294)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:130)
	at com.google.gson.Gson.getAdapter(Gson.java:556)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.createBoundField(ReflectiveTypeAdapterFactory.java:160)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:294)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:130)
	at com.google.gson.Gson.getAdapter(Gson.java:556)
	at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:55)
	at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:97)
	at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:61)
	at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:70)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:196)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:368)
	at com.google.gson.Gson.toJson(Gson.java:842)
	at com.google.gson.Gson.toJsonTree(Gson.java:712)
	at com.google.gson.Gson.toJsonTree(Gson.java:689)
	at com.microsoft.playwright.impl.ElementHandleImpl.screenshotImpl(ElementHandleImpl.java:349)
	at com.microsoft.playwright.impl.ElementHandleImpl.lambda$screenshot$25(ElementHandleImpl.java:329)
	at com.microsoft.playwright.impl.LoggingSupport.withLogging(LoggingSupport.java:47)
	at com.microsoft.playwright.impl.ChannelOwner.withLogging(ChannelOwner.java:97)
	at com.microsoft.playwright.impl.ElementHandleImpl.screenshot(ElementHandleImpl.java:329)
	at com.microsoft.playwright.impl.LocatorImpl.lambda$screenshot$5(LocatorImpl.java:486)
	at com.microsoft.playwright.impl.LocatorImpl.withElement(LocatorImpl.java:85)
	at com.microsoft.playwright.impl.LocatorImpl.screenshot(LocatorImpl.java:486)
	at LocatorScreenshotBugReproducer.main(LocatorScreenshotBugReproducer.java:22)
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.lang.Throwable.detailMessage accessible: module java.base does not "opens java.lang" to unnamed module @59fa0ced
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
	at com.google.gson.internal.reflect.ReflectionHelper.makeAccessible(ReflectionHelper.java:35)
	... 47 more
```

### Additional context

Bug analysis:
The Locator screenshot functionality forgets to remove the `mask` field from the generic Gson serialization of the options, and Gson finally fails because deep inside the `mask` field (which is a `Locator`) is an `Exception` field which cannot be serialized.

The Page screenshot code has handled this by special-casing the `mask` field. This needs to be done also for the Locator code (actually located in `ElementHandleImpl`).

Working screenshot code:
https://github.com/microsoft/playwright-java/blob/fddb146d731cd3c1a11260bc715ad20eaf7a2d46/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java#L1211-L1222

Special handling for `mask` missing:
https://github.com/microsoft/playwright-java/blob/fddb146d731cd3c1a11260bc715ad20eaf7a2d46/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java#L332

### Environment

Java 17


Discussion:
Any chance of having a look at this? Should be an easy fix. Can I do a PR for this? I would just mirror the implementation of `PageImpl`.
I created a PR for this simple fix. Could you please have a look at it? This bug is really blocking us from proceeding with our Playwright testing currently.



Patch:
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java
index 30a8ae57..cf23e4c0 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java
@@ -1166,18 +1166,8 @@ private byte[] screenshotImpl(ScreenshotOptions options) {
         }
       }
     }
-    List<Locator> mask = options.mask;
-    options.mask = null;
     JsonObject params = gson().toJsonTree(options).getAsJsonObject();
-    options.mask = mask;
     params.remove("path");
-    if (mask != null) {
-      JsonArray maskArray = new JsonArray();
-      for (Locator locator: mask) {
-        maskArray.add(((LocatorImpl) locator).toProtocol());
-      }
-      params.add("mask", maskArray);
-    }
     JsonObject json = sendMessage("screenshot", params, timeoutSettings.timeout(options.timeout)).getAsJsonObject();
 
     byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString());
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java
index 633e7576..8be93f1c 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java
@@ -66,6 +66,7 @@ class Serialization {
     .registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
     .registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer())
     .registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer())
+    .registerTypeAdapter(LocatorImpl.class, new LocatorImplSerializer())
     .registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();
 
   static Gson gson() {
@@ -490,6 +491,13 @@ public JsonElement serialize(E src, Type typeOfSrc, JsonSerializationContext con
     }
   }
 
+  private static class LocatorImplSerializer implements JsonSerializer<LocatorImpl> {
+    @Override
+    public JsonElement serialize(LocatorImpl src, Type typeOfSrc, JsonSerializationContext context) {
+      return src.toProtocol();
+    }
+  }
+
   private static class SameSiteAdapter extends TypeAdapter<SameSiteAttribute> {
     @Override
     public void write(JsonWriter out, SameSiteAttribute value) throws IOException {


Test patch:
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageScreenshot.java b/playwright/src/test/java/com/microsoft/playwright/TestPageScreenshot.java
index a756476b..2e82c920 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestPageScreenshot.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestPageScreenshot.java
@@ -136,7 +136,7 @@ void shouldNotCaptureInfiniteWebAnimations() {
   }
 
   @Test
-  void maskShouldWork() {
+  void maskShouldWorkForPage() {
     page.setViewportSize(500, 500);
     page.navigate(server.PREFIX + "/grid.html");
     byte[] screenshot = page.screenshot(new Page.ScreenshotOptions()
@@ -146,6 +146,17 @@ void maskShouldWork() {
     assertThrows(AssertionFailedError.class, () -> assertArrayEquals(screenshot, originalScreenshot));
   }
 
+  @Test
+  void maskShouldWorkForLocator() {
+    page.navigate(server.PREFIX + "/grid.html");
+    Locator locatorToScreenshot = page.locator("div").first();
+    byte[] screenshot = locatorToScreenshot.screenshot(new Locator.ScreenshotOptions()
+      .setMask(asList(page.locator("img"))));
+    // TODO: toMatchSnapshot is not present in java, so we only checks that masked screenshot is different.
+    byte[] originalScreenshot = locatorToScreenshot.screenshot();
+    assertThrows(AssertionFailedError.class, () -> assertArrayEquals(screenshot, originalScreenshot));
+  }
+
   @Test
   void shouldWorkWithDeviceScaleFactorAndClip() {
     try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()