playwright-java icon indicating copy to clipboard operation
playwright-java copied to clipboard

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

Open janne-hyoetylae-med opened this issue 6 months ago • 0 comments

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

janne-hyoetylae-med avatar May 13 '25 15:05 janne-hyoetylae-med