diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6987f91..13383842 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,11 @@ jobs: run: mvn test --no-transfer-progress --fail-at-end env: BROWSER: ${{ matrix.browser }} + - name: Run tracing tests w/ sources + run: mvn test --no-transfer-progress --fail-at-end -D test=*TestTracing* + env: + BROWSER: ${{ matrix.browser }} + PLAYWRIGHT_JAVA_SRC: src/test/java - name: Test Spring Boot Starter shell: bash env: diff --git a/README.md b/README.md index 567cb306..e462fdbd 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | :--- | :---: | :---: | :---: | | Chromium 99.0.4763.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 15.4 | ✅ | ✅ | ✅ | -| Firefox 94.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 95.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/next/intro/#system-requirements) for details. diff --git a/assertions/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java b/assertions/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java index 363a71ff..9b52daf0 100644 --- a/assertions/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java +++ b/assertions/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java @@ -22,7 +22,7 @@ import com.microsoft.playwright.Page; /** * The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page} state in the - * tests. A new instance of {@code LocatorAssertions} is created by calling {@link PlaywrightAssertions#assertThat + * tests. A new instance of {@code PageAssertions} is created by calling {@link PlaywrightAssertions#assertThat * PlaywrightAssertions.assertThat()}: *
{@code
  * ...
diff --git a/playwright/src/main/java/com/microsoft/playwright/Tracing.java b/playwright/src/main/java/com/microsoft/playwright/Tracing.java
index 2418e057..f57d6865 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Tracing.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Tracing.java
@@ -51,6 +51,11 @@ public interface Tracing {
      * Whether to capture DOM snapshot on every action.
      */
     public Boolean snapshots;
+    /**
+     * Whether to include source files for trace actions. List of the directories with source code for the application must be
+     * provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable.
+     */
+    public Boolean sources;
     /**
      * Trace name to be shown in the Trace Viewer.
      */
@@ -78,6 +83,14 @@ public interface Tracing {
       this.snapshots = snapshots;
       return this;
     }
+    /**
+     * Whether to include source files for trace actions. List of the directories with source code for the application must be
+     * provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable.
+     */
+    public StartOptions setSources(boolean sources) {
+      this.sources = sources;
+      return this;
+    }
     /**
      * Trace name to be shown in the Trace Viewer.
      */
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
index 4f5f6ba4..fc81d344 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
@@ -16,23 +16,18 @@
 package com.microsoft.playwright.impl;
 
 import com.google.gson.Gson;
-import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.microsoft.playwright.Playwright;
 import com.microsoft.playwright.PlaywrightException;
 import com.microsoft.playwright.TimeoutError;
 
-import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.time.Duration;
 import java.util.HashMap;
 import java.util.Map;
 
-import static com.microsoft.playwright.impl.LoggingSupport.logWithTimestamp;
 import static com.microsoft.playwright.impl.Serialization.gson;
 
 class Message {
@@ -62,7 +57,7 @@ public class Connection {
   private final Map objects = new HashMap<>();
   private final Root root;
   private int lastId = 0;
-  private final Path srcDir;
+  private final StackTraceCollector stackTraceCollector;
   private final Map> callbacks = new HashMap<>();
   private String apiName;
   private static final boolean isLogging;
@@ -91,14 +86,11 @@ public class Connection {
     this.transport = transport;
     root = new Root(this);
     String srcRoot = System.getenv("PLAYWRIGHT_JAVA_SRC");
-    if (srcRoot == null) {
-      srcDir = null;
-    } else {
-      srcDir = Paths.get(srcRoot);
-      if (!Files.exists(srcDir)) {
-        throw new PlaywrightException("PLAYWRIGHT_JAVA_SRC environment variable points to non-existing location: '" + srcRoot + "'");
-      }
-    }
+    stackTraceCollector = (srcRoot == null) ? null: new StackTraceCollector(Paths.get(srcRoot));
+  }
+
+  boolean isCollectingStacks() {
+    return stackTraceCollector != null;
   }
 
   String setApiName(String name) {
@@ -119,45 +111,6 @@ public class Connection {
     return internalSendMessage(guid, method, params);
   }
 
-  private String sourceFile(StackTraceElement frame) {
-    String pkg = frame.getClassName();
-    int lastDot = pkg.lastIndexOf('.');
-    if (lastDot == -1) {
-      pkg = "";
-    } else {
-      pkg = frame.getClassName().substring(0, lastDot + 1);
-    }
-    pkg = pkg.replace('.', File.separatorChar);
-    return srcDir.resolve(pkg).resolve(frame.getFileName()).toString();
-  }
-
-  private JsonArray currentStackTrace() {
-    StackTraceElement[] stack = Thread.currentThread().getStackTrace();
-
-    int index = 0;
-    while (index < stack.length && !stack[index].getClassName().equals(getClass().getName())) {
-      index++;
-    };
-    // Find Playwright API call
-    while (index < stack.length && stack[index].getClassName().startsWith("com.microsoft.playwright.")) {
-      // hack for tests
-      if (stack[index].getClassName().startsWith("com.microsoft.playwright.Test")) {
-        break;
-      }
-      index++;
-    }
-    JsonArray jsonStack = new JsonArray();
-    for (; index < stack.length; index++) {
-      StackTraceElement frame = stack[index];
-      JsonObject jsonFrame = new JsonObject();
-      jsonFrame.addProperty("file", sourceFile(frame));
-      jsonFrame.addProperty("line", frame.getLineNumber());
-      jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
-      jsonStack.add(jsonFrame);
-    }
-    return jsonStack;
-  }
-
   private WaitableResult internalSendMessage(String guid, String method, JsonObject params) {
     int id = ++lastId;
     WaitableResult result = new WaitableResult<>();
@@ -168,8 +121,8 @@ public class Connection {
     message.addProperty("method", method);
     message.add("params", params);
     JsonObject metadata = new JsonObject();
-    if (srcDir != null) {
-      metadata.add("stack", currentStackTrace());
+    if (stackTraceCollector != null) {
+      metadata.add("stack", stackTraceCollector.currentStackTrace());
     }
     if (apiName != null) {
       metadata.addProperty("apiName", apiName);
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/StackTraceCollector.java b/playwright/src/main/java/com/microsoft/playwright/impl/StackTraceCollector.java
new file mode 100644
index 00000000..93c67eaf
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/StackTraceCollector.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright.impl;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.microsoft.playwright.PlaywrightException;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+class StackTraceCollector {
+  private final Path srcDir;
+
+  StackTraceCollector(Path srcDir) {
+    if (!Files.exists(srcDir.toAbsolutePath())) {
+      throw new PlaywrightException("Source location doesn't exist: '" + srcDir.toAbsolutePath() + "'");
+    }
+    this.srcDir = srcDir;
+  }
+
+  private String sourceFile(StackTraceElement frame) {
+    String pkg = frame.getClassName();
+    int lastDot = pkg.lastIndexOf('.');
+    if (lastDot == -1) {
+      pkg = "";
+    } else {
+      pkg = frame.getClassName().substring(0, lastDot + 1);
+    }
+    pkg = pkg.replace('.', File.separatorChar);
+    String file = frame.getFileName();
+    if (file == null) {
+      return "";
+    }
+    return srcDir.resolve(pkg).resolve(file).toString();
+  }
+
+  JsonArray currentStackTrace() {
+    StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+
+    int index = 0;
+    while (index < stack.length && !stack[index].getClassName().equals(getClass().getName())) {
+      index++;
+    };
+    // Find Playwright API call
+    while (index < stack.length && stack[index].getClassName().startsWith("com.microsoft.playwright.")) {
+      // hack for tests
+      if (stack[index].getClassName().startsWith("com.microsoft.playwright.Test")) {
+        break;
+      }
+      index++;
+    }
+    JsonArray jsonStack = new JsonArray();
+    for (; index < stack.length; index++) {
+      StackTraceElement frame = stack[index];
+      JsonObject jsonFrame = new JsonObject();
+      jsonFrame.addProperty("file", sourceFile(frame));
+      jsonFrame.addProperty("line", frame.getLineNumber());
+      jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
+      jsonStack.add(jsonFrame);
+    }
+    return jsonStack;
+  }
+
+
+}
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java
index 26597996..c476ff4a 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java
@@ -16,8 +16,8 @@
 
 package com.microsoft.playwright.impl;
 
-import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
+import com.microsoft.playwright.PlaywrightException;
 import com.microsoft.playwright.Tracing;
 
 import java.nio.file.Path;
@@ -26,16 +26,23 @@ import static com.microsoft.playwright.impl.Serialization.gson;
 
 class TracingImpl implements Tracing {
   private final BrowserContextImpl context;
+  private boolean includeSources;
 
   TracingImpl(BrowserContextImpl context) {
     this.context = context;
   }
 
   private void stopChunkImpl(Path path) {
+    boolean isRemote = context.browser() != null && context.browser().isRemote;
     JsonObject params = new JsonObject();
     String mode = "doNotSave";
     if (path != null) {
-      mode = "compressTrace";
+      if (isRemote) {
+        mode = "compressTrace";
+      } else {
+        // TODO: support source zips and do compression on the client.
+        mode = "compressTraceAndSources";
+      }
     }
     params.addProperty("mode", mode);
     JsonObject json = context.sendMessage("tracingStopChunk", params).getAsJsonObject();
@@ -45,7 +52,7 @@ class TracingImpl implements Tracing {
     ArtifactImpl artifact = context.connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
     // In case of CDP connection browser is null but since the connection is established by
     // the driver it is safe to consider the artifact local.
-    if (context.browser() != null && context.browser().isRemote) {
+    if (isRemote) {
       artifact.isRemote = true;
     }
     artifact.saveAs(path);
@@ -77,6 +84,13 @@ class TracingImpl implements Tracing {
       options = new StartOptions();
     }
     JsonObject params = gson().toJsonTree(options).getAsJsonObject();
+    includeSources = options.sources != null;
+    if (includeSources) {
+      if (!context.connection.isCollectingStacks()) {
+        throw new PlaywrightException("Source root directories must be provided to enable source collection");
+      }
+      params.addProperty("sources", true);
+    }
     context.sendMessage("tracingStart", params);
     context.sendMessage("tracingStartChunk");
   }
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestClick.java b/playwright/src/test/java/com/microsoft/playwright/TestClick.java
index 8be6c150..50ae49f5 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestClick.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestClick.java
@@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledIf;
 import org.junit.jupiter.api.condition.EnabledIf;
 
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java
index fa4acd92..9965e4ba 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java
@@ -45,7 +45,7 @@ public class TestElementHandleBoundingBox extends TestBase {
 
   @Test
   void shouldHandleNestedFrames() {
-    page.setViewportSize(500, 500);
+    page.setViewportSize(616, 500);
     page.navigate(server.PREFIX + "/frames/nested-frames.html");
     Frame nestedFrame = page.frame("dos");
     assertNotNull(nestedFrame);
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java
index c6f4e6a9..85cc6725 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java
@@ -16,17 +16,31 @@
 
 package com.microsoft.playwright;
 
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
+import java.io.*;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.zip.GZIPOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
 
+import static com.microsoft.playwright.Utils.copy;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class TestTracing extends TestBase {
 
+  private static final String _PW_JAVA_TEST_SRC = "_PW_JAVA_TEST_SRC";
+
   @Override
   @BeforeAll
   void launchBrowser() {
@@ -96,4 +110,39 @@ public class TestTracing extends TestBase {
     assertTrue(Files.exists(traceFile1));
     assertTrue(Files.exists(traceFile2));
   }
+
+  @Test
+  void shouldCollectSources(@TempDir Path tmpDir) throws IOException {
+    Assumptions.assumeTrue(System.getenv("PLAYWRIGHT_JAVA_SRC") != null, "PLAYWRIGHT_JAVA_SRC must point to the directory containing this test source.");
+    context.tracing().start(new Tracing.StartOptions().setSources(true));
+    page.navigate(server.EMPTY_PAGE);
+    page.setContent("");
+    page.click("'Click'");
+    Path trace = tmpDir.resolve("trace1.zip");
+    context.tracing().stop(new Tracing.StopOptions().setPath(trace));
+
+    Map entries = parseTrace(trace);
+    Map sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    assertEquals(1, sources.size());
+
+    String path = getClass().getName().replace('.', File.separatorChar);
+    Path sourceFile = Paths.get(System.getenv("PLAYWRIGHT_JAVA_SRC"), path + ".java");
+    byte[] thisFile = Files.readAllBytes(sourceFile);
+    assertEquals(new String(thisFile, UTF_8), new String(sources.values().iterator().next(), UTF_8));
+  }
+
+  private static Map parseTrace(Path trace) throws IOException {
+    Map entries = new HashMap<>();
+    try (ZipInputStream zis = new ZipInputStream(new FileInputStream(trace.toFile()))) {
+      for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) {
+        ByteArrayOutputStream content = new ByteArrayOutputStream();
+        try (OutputStream output = content) {
+          copy(zis, output);
+        }
+        entries.put(zipEntry.getName(), content.toByteArray());
+      }
+      zis.closeEntry();
+    }
+    return entries;
+  }
 }
diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION
index fada3a67..7ecc7214 100644
--- a/scripts/CLI_VERSION
+++ b/scripts/CLI_VERSION
@@ -1 +1 @@
-1.18.0-alpha-dec-16-2021
+1.18.0-alpha-1639770748000