From 41bff4da1bbf8db6f46c17e532791841fa4b530f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 25 Aug 2025 15:38:44 +0200 Subject: [PATCH 1/6] chore: migrate Trace Viewer tests to use real Trace viewer --- .../java/com/microsoft/playwright/Server.java | 21 +- .../com/microsoft/playwright/TestTracing.java | 322 +++++++++++++----- 2 files changed, 255 insertions(+), 88 deletions(-) diff --git a/playwright/src/test/java/com/microsoft/playwright/Server.java b/playwright/src/test/java/com/microsoft/playwright/Server.java index beeea69d8..e136df75f 100644 --- a/playwright/src/test/java/com/microsoft/playwright/Server.java +++ b/playwright/src/test/java/com/microsoft/playwright/Server.java @@ -20,6 +20,8 @@ import java.io.*; import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -40,6 +42,7 @@ public class Server implements HttpHandler { private final Map csp = Collections.synchronizedMap(new HashMap<>()); private final Map routes = Collections.synchronizedMap(new HashMap<>()); private final Set gzipRoutes = Collections.synchronizedSet(new HashSet<>()); + private Path staticFilesDirectory; private static class Auth { public final String user; @@ -93,6 +96,10 @@ void enableGzip(String path) { gzipRoutes.add(path); } + void setStaticFilesDirectory(Path staticFilesDirectory) { + this.staticFilesDirectory = staticFilesDirectory; + } + static class Request { public final String url; public final String method; @@ -187,7 +194,19 @@ public void handle(HttpExchange exchange) throws IOException { path = "/index.html"; } - // Resources from "src/test/resources/" are copied to "resources/" directory in the jar. + // If static files directory is set, serve from filesystem first + if (staticFilesDirectory != null) { + Path filePath = staticFilesDirectory.resolve(path.substring(1)); // Remove leading / + if (Files.exists(filePath) && !Files.isDirectory(filePath)) { + exchange.getResponseHeaders().add("Content-Type", mimeType(filePath.toFile())); + exchange.sendResponseHeaders(200, Files.size(filePath)); + Files.copy(filePath, exchange.getResponseBody()); + exchange.getResponseBody().close(); + return; + } + } + + // Fallback: Resources from "src/test/resources/" are copied to "resources/" directory in the jar. String resourcePath = "resources" + path; InputStream resource = getClass().getClassLoader().getResourceAsStream(resourcePath); if (resource == null) { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java index 4b22abd27..e56318a64 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java @@ -16,8 +16,8 @@ package com.microsoft.playwright; -import com.google.gson.Gson; -import com.google.gson.annotations.SerializedName; +import com.microsoft.playwright.BrowserType.LaunchOptions; +import com.microsoft.playwright.impl.driver.Driver; import com.microsoft.playwright.options.AriaRole; import com.microsoft.playwright.options.Location; import com.microsoft.playwright.options.MouseButton; @@ -31,14 +31,9 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Arrays.asList; +import java.util.regex.Pattern; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import static org.junit.jupiter.api.Assertions.*; public class TestTracing extends TestBase { @@ -57,7 +52,7 @@ void launchBrowser(@TempDir Path tempDir) { } @Test - void shouldCollectTrace1(@TempDir Path tempDir) { + void shouldCollectTrace1(@TempDir Path tempDir) throws Exception { context.tracing().start(new Tracing.StartOptions().setName("test") .setScreenshots(true).setSnapshots(true)); page.navigate(server.EMPTY_PAGE); @@ -68,10 +63,18 @@ void shouldCollectTrace1(@TempDir Path tempDir) { context.tracing().stop(new Tracing.StopOptions().setPath(traceFile)); assertTrue(Files.exists(traceFile)); + showTraceViewer(traceFile, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Navigate to \"/empty.html\""), + Pattern.compile("Set content"), + Pattern.compile("Click"), + Pattern.compile("Close") + }); + }); } @Test - void shouldCollectTwoTraces(@TempDir Path tempDir) { + void shouldCollectTwoTraces(@TempDir Path tempDir) throws Exception { context.tracing().start(new Tracing.StartOptions().setName("test1") .setScreenshots(true).setSnapshots(true)); page.navigate(server.EMPTY_PAGE); @@ -89,10 +92,25 @@ void shouldCollectTwoTraces(@TempDir Path tempDir) { assertTrue(Files.exists(traceFile1)); assertTrue(Files.exists(traceFile2)); + + showTraceViewer(traceFile1, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Navigate to \"/empty.html\""), + Pattern.compile("Set content"), + Pattern.compile("Click") + }); + }); + + showTraceViewer(traceFile2, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Double click"), + Pattern.compile("Close") + }); + }); } @Test - void shouldWorkWithMultipleChunks(@TempDir Path tempDir) { + void shouldWorkWithMultipleChunks(@TempDir Path tempDir) throws Exception { context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)); page.navigate(server.PREFIX + "/frames/frame.html"); @@ -109,28 +127,60 @@ void shouldWorkWithMultipleChunks(@TempDir Path tempDir) { assertTrue(Files.exists(traceFile1)); assertTrue(Files.exists(traceFile2)); + + showTraceViewer(traceFile1, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Set content"), + Pattern.compile("Click") + }); + traceViewer.selectSnapshot("After"); + FrameLocator frame = traceViewer.snapshotFrame("Set content", 0, false); + assertThat(frame.locator("button")).hasText("Click"); + }); + + showTraceViewer(traceFile2, traceViewer -> { + assertThat(traceViewer.actionTitles()).containsText(new String[] {"Hover"}); + FrameLocator frame = traceViewer.snapshotFrame("Hover", 0, false); + assertThat(frame.locator("button")).hasText("Click"); + }); } @Test - void shouldCollectSources(@TempDir Path tmpDir) throws IOException { + void shouldCollectSources(@TempDir Path tmpDir) throws Exception { 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'"); + myMethodOuter(); Path trace = tmpDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(trace)); - Map entries = Utils.parseZip(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()); + showTraceViewer(trace, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Navigate to \"/empty.html\""), + Pattern.compile("Set content"), + Pattern.compile("Click") + }); + traceViewer.showSourceTab(); + assertThat(traceViewer.stackFrames()).containsText(new Pattern[] { + Pattern.compile("myMethodInner"), + Pattern.compile("myMethodOuter"), + Pattern.compile("shouldCollectSources") + }); + traceViewer.selectAction("Set content"); + assertThat(traceViewer.page().locator(".source-tab-file-name")) + .hasAttribute("title", Pattern.compile(".*TestTracing\\.java")); + assertThat(traceViewer.page().locator(".source-line-running")) + .containsText("page.setContent(\"\");"); + }); + } + + private void myMethodOuter() { + myMethodInner(); + } - String path = getClass().getName().replace('.', File.separatorChar); - String[] srcRoots = System.getenv("PLAYWRIGHT_JAVA_SRC").split(File.pathSeparator); - // Resolve in the last specified source dir. - Path sourceFile = Paths.get(srcRoots[srcRoots.length - 1], path + ".java"); - byte[] thisFile = Files.readAllBytes(sourceFile); - assertEquals(new String(thisFile, UTF_8), new String(sources.values().iterator().next(), UTF_8)); + private void myMethodInner() { + page.getByText("Click").click(); } @Test @@ -140,7 +190,7 @@ void shouldNotFailWhenSourcesSetExplicitlyToFalse() throws IOException { } @Test - void shouldRespectTracesDirAndName(@TempDir Path tempDir) { + void shouldRespectTracesDirAndName(@TempDir Path tempDir) throws Exception { Path tracesDir = tempDir.resolve("trace-dir"); BrowserType.LaunchOptions options = createLaunchOptions(); options.setTracesDir(tracesDir); @@ -159,6 +209,24 @@ void shouldRespectTracesDirAndName(@TempDir Path tempDir) { context.tracing().stop(new Tracing.StopOptions().setPath(tempDir.resolve("trace2.zip"))); assertTrue(Files.exists(tracesDir.resolve("name2.trace"))); assertTrue(Files.exists(tracesDir.resolve("name2.network"))); + + showTraceViewer(tempDir.resolve("trace1.zip"), traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Navigate to \"/one-style.html\"") + }); + FrameLocator frame = traceViewer.snapshotFrame("Navigate", 0, false); + assertThat(frame.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)"); + assertThat(frame.locator("body")).hasText("hello, world!"); + }); + + showTraceViewer(tempDir.resolve("trace2.zip"), traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Navigate to \"/har.html\"") + }); + FrameLocator frame = traceViewer.snapshotFrame("Navigate", 0, false); + assertThat(frame.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)"); + assertThat(frame.locator("body")).hasText("hello, world!"); + }); } } @@ -179,11 +247,9 @@ void canCallTracingGroupGroupEndAtAnyTimeAndAutoClose(@TempDir Path tempDir) thr context.tracing().groupEnd(); context.tracing().groupEnd(); - List events = parseTraceEvents(traceFile1); - List groups = events.stream().filter(e -> "tracingGroup".equals(e.method)).collect(Collectors.toList()); - assertEquals(1, groups.size()); - assertEquals("actual", groups.get(0).title); - + showTraceViewer(traceFile1, traceViewer -> { + assertThat(traceViewer.actionTitles()).containsText(new String[] {"actual", "Navigate to \"/empty.html\""}); + }); } @Test @@ -202,9 +268,17 @@ void traceGroupGroupEnd(@TempDir Path tempDir) throws Exception { Path traceFile1 = tempDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); - List events = parseTraceEvents(traceFile1); - List calls = events.stream().filter(e -> e.renderedTitle() != null).map(e -> e.renderedTitle()).collect(Collectors.toList()); - assertEquals(asList("outer group", "Frame.goto", "inner group 1", "Frame.click", "inner group 2", "Frame.isVisible"), calls); + showTraceViewer(traceFile1, traceViewer -> { + traceViewer.expandAction("inner group 1"); + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("outer group"), + Pattern.compile("Navigate to \"data:"), + Pattern.compile("inner group 1"), + Pattern.compile("Click"), + Pattern.compile("inner group 2"), + Pattern.compile("Is visible") + }); + }); } @Test @@ -240,64 +314,138 @@ void shouldTraceVariousAPIs(@TempDir Path tempDir) throws Exception { Path traceFile1 = tempDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); - List events = parseTraceEvents(traceFile1); - List calls = events.stream().filter(e -> e.renderedTitle() != null).map(e -> e.renderedTitle()) - .collect(Collectors.toList()); - assertEquals(asList( - "BrowserContext.clockInstall", - "Frame.setContent", - "Frame.click", - "Frame.click", - "Page.keyboardType", - "Page.keyboardPress", - "Page.keyboardDown", - "Page.keyboardInsertText", - "Page.keyboardUp", - "Page.mouseMove", - "Page.mouseDown", - "Page.mouseMove", - "Page.mouseWheel", - "Page.mouseUp", - "BrowserContext.clockFastForward", - "BrowserContext.clockFastForward", - "BrowserContext.clockPauseAt", - "BrowserContext.clockRunFor", - "BrowserContext.clockSetFixedTime", - "BrowserContext.clockSetSystemTime", - "BrowserContext.clockResume", - "Frame.click"), - calls); + showTraceViewer(traceFile1, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Install clock"), + Pattern.compile("Set content"), + Pattern.compile("Click"), + Pattern.compile("Click"), + Pattern.compile("Type"), + Pattern.compile("Press"), + Pattern.compile("Key down"), + Pattern.compile("Insert"), + Pattern.compile("Key up"), + Pattern.compile("Mouse move"), + Pattern.compile("Mouse down"), + Pattern.compile("Mouse move"), + Pattern.compile("Mouse wheel"), + Pattern.compile("Mouse up"), + Pattern.compile("Fast forward clock"), + Pattern.compile("Fast forward clock"), + Pattern.compile("Pause clock"), + Pattern.compile("Run clock"), + Pattern.compile("Set fixed time"), + Pattern.compile("Set system time"), + Pattern.compile("Resume clock"), + Pattern.compile("Click") + }); + }); + } + + @Test + public void shouldNotRecordNetworkActions(@TempDir Path tempDir) throws Exception { + context.tracing().start(new Tracing.StartOptions()); + + page.onRequest(request -> { + request.allHeaders(); + }); + page.onResponse(response -> { + response.text(); + }); + page.navigate(server.EMPTY_PAGE); + + Path traceFile1 = tempDir.resolve("trace1.zip"); + context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); + + showTraceViewer(traceFile1, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Navigate to \"/empty.html\"") + }); + }); } - private static class TraceEvent { - String type; - String name; - String title; - @SerializedName("class") - String clazz; - String method; - Double startTime; - Double endTime; - String callId; - - String renderedTitle() { - if (title != null) { - return title; - } - if (clazz != null && method != null) { - return clazz + "." + method; - } - return null; + private void showTraceViewer(Path tracePath, TraceViewerConsumer callback) throws Exception { + Path driverDir = Driver.ensureDriverInstalled(java.util.Collections.emptyMap(), true).driverDir(); + Path traceViewerPath = driverDir.resolve("package").resolve("lib").resolve("vite").resolve("traceViewer"); + Server traceServer = Server.createHttp(Utils.nextFreePort()); + traceServer.setStaticFilesDirectory(traceViewerPath); + traceServer.setRoute("/trace.zip", exchange -> { + exchange.getResponseHeaders().add("Content-Type", "application/zip"); + exchange.sendResponseHeaders(200, Files.size(tracePath)); + Files.copy(tracePath, exchange.getResponseBody()); + exchange.getResponseBody().close(); + }); + + try (Browser browser = browserType.launch(createLaunchOptions()); + BrowserContext context = browser.newContext()) { + Page page = context.newPage(); + page.navigate(traceServer.PREFIX + "/index.html?trace=" + traceServer.PREFIX + "/trace.zip"); + + TraceViewerPage traceViewer = new TraceViewerPage(page); + callback.accept(traceViewer); + } finally { + traceServer.stop(); } } + + @FunctionalInterface + interface TraceViewerConsumer { + void accept(TraceViewerPage traceViewer) throws Exception; + } +} + +class TraceViewerPage { + private final Page page; + + public TraceViewerPage(Page page) { + this.page = page; + } + + public Page page() { + return page; + } + + public Locator actionsTree() { + return page.getByTestId("actions-tree"); + } + + public Locator actionTitles() { + return page.locator(".action-title"); + } + + public Locator stackFrames() { + return this.page.getByRole(AriaRole.LIST, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.LISTITEM); + } + + public void selectAction(String title, int ordinal) { + this.actionsTree().getByTitle(title).nth(ordinal).click(); + } + + public void selectAction(String title) { + selectAction(title, 0); + } + + public void selectSnapshot(String name) { + this.page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName(name)).click(); + } + + public FrameLocator snapshotFrame(String actionName, int ordinal, boolean hasSubframe) { + selectAction(actionName, ordinal); + while (page.frames().size() < (hasSubframe ? 4 : 3)) { + page.waitForTimeout(200); + } + return page.frameLocator("iframe.snapshot-visible[name=snapshot]"); + } + + public FrameLocator snapshotFrame(String actionName, int ordinal) { + return snapshotFrame(actionName, ordinal, false); + } + + public void showSourceTab() { + page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName("Source")).click(); + } - private static List parseTraceEvents(Path traceFile) throws IOException { - Map files = Utils.parseZip(traceFile); - Map traces = files.entrySet().stream().filter(e -> e.getKey().endsWith(".trace")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - assertNotNull(traces.get("trace.trace")); - return Arrays.stream(new String(traces.get("trace.trace"), UTF_8) - .split("\n")) - .map(s -> new Gson().fromJson(s, TraceEvent.class)) - .collect(Collectors.toList()); + public void expandAction(String title) { + this.actionsTree().getByRole(AriaRole.TREEITEM, new Locator.GetByRoleOptions().setName(title)).locator(".codicon-chevron-right").click(); } } From 4da161153aa6e649c62bc83e664e2e14ac4e0033 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 25 Aug 2025 15:53:23 +0200 Subject: [PATCH 2/6] nit --- .../java/com/microsoft/playwright/Server.java | 31 ++++++------------- .../com/microsoft/playwright/TestTracing.java | 12 ++++++- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/playwright/src/test/java/com/microsoft/playwright/Server.java b/playwright/src/test/java/com/microsoft/playwright/Server.java index e136df75f..c8b6838a7 100644 --- a/playwright/src/test/java/com/microsoft/playwright/Server.java +++ b/playwright/src/test/java/com/microsoft/playwright/Server.java @@ -20,11 +20,10 @@ import java.io.*; import java.net.InetSocketAddress; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.function.Function; import java.util.zip.GZIPOutputStream; import static com.microsoft.playwright.Utils.copy; @@ -42,7 +41,7 @@ public class Server implements HttpHandler { private final Map csp = Collections.synchronizedMap(new HashMap<>()); private final Map routes = Collections.synchronizedMap(new HashMap<>()); private final Set gzipRoutes = Collections.synchronizedSet(new HashSet<>()); - private Path staticFilesDirectory; + private Function resourceProvider; private static class Auth { public final String user; @@ -78,6 +77,8 @@ private Server(int port, boolean https) throws IOException { server.createContext("/", this); server.setExecutor(null); // creates a default executor server.start(); + // Resources from "src/test/resources/" are copied to "resources/" directory in the jar. + resourceProvider = path -> Server.class.getClassLoader().getResourceAsStream("resources" + path); } public void stop() { @@ -96,8 +97,8 @@ void enableGzip(String path) { gzipRoutes.add(path); } - void setStaticFilesDirectory(Path staticFilesDirectory) { - this.staticFilesDirectory = staticFilesDirectory; + void setResourceProvider(Function resourceProvider) { + this.resourceProvider = resourceProvider; } static class Request { @@ -194,30 +195,16 @@ public void handle(HttpExchange exchange) throws IOException { path = "/index.html"; } - // If static files directory is set, serve from filesystem first - if (staticFilesDirectory != null) { - Path filePath = staticFilesDirectory.resolve(path.substring(1)); // Remove leading / - if (Files.exists(filePath) && !Files.isDirectory(filePath)) { - exchange.getResponseHeaders().add("Content-Type", mimeType(filePath.toFile())); - exchange.sendResponseHeaders(200, Files.size(filePath)); - Files.copy(filePath, exchange.getResponseBody()); - exchange.getResponseBody().close(); - return; - } - } - - // Fallback: Resources from "src/test/resources/" are copied to "resources/" directory in the jar. - String resourcePath = "resources" + path; - InputStream resource = getClass().getClassLoader().getResourceAsStream(resourcePath); + InputStream resource = resourceProvider.apply(path); if (resource == null) { exchange.getResponseHeaders().add("Content-Type", "text/plain"); exchange.sendResponseHeaders(404, 0); try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { - writer.write("File not found: " + resourcePath); + writer.write("File not found: " + path); } return; } - exchange.getResponseHeaders().add("Content-Type", mimeType(new File(resourcePath))); + exchange.getResponseHeaders().add("Content-Type", mimeType(new File(path))); ByteArrayOutputStream body = new ByteArrayOutputStream(); OutputStream output = body; if (gzipRoutes.contains(path)) { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java index e56318a64..2857e5d78 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java @@ -368,7 +368,17 @@ private void showTraceViewer(Path tracePath, TraceViewerConsumer callback) throw Path driverDir = Driver.ensureDriverInstalled(java.util.Collections.emptyMap(), true).driverDir(); Path traceViewerPath = driverDir.resolve("package").resolve("lib").resolve("vite").resolve("traceViewer"); Server traceServer = Server.createHttp(Utils.nextFreePort()); - traceServer.setStaticFilesDirectory(traceViewerPath); + traceServer.setResourceProvider(path -> { + Path filePath = traceViewerPath.resolve(path.substring(1)); + if (Files.exists(filePath) && !Files.isDirectory(filePath)) { + try { + return Files.newInputStream(filePath); + } catch (IOException e) { + return null; + } + } + return null; + }); traceServer.setRoute("/trace.zip", exchange -> { exchange.getResponseHeaders().add("Content-Type", "application/zip"); exchange.sendResponseHeaders(200, Files.size(tracePath)); From 8b1d73e40bb838e8bf428586d7e1a1f0de83c0f4 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 25 Aug 2025 23:13:56 +0200 Subject: [PATCH 3/6] test --- .../com/microsoft/playwright/impl/driver/jar/DriverJar.java | 4 ++-- .../java/com/microsoft/playwright/TraceViewerFixture.java | 0 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TraceViewerFixture.java diff --git a/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java b/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java index 654ff6732..ab2d221c0 100644 --- a/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java +++ b/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java @@ -40,7 +40,6 @@ public DriverJar() throws IOException { driverTempDir = alternativeTmpdir == null ? Files.createTempDirectory(prefix) : Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix); - driverTempDir.toFile().deleteOnExit(); String nodePath = System.getProperty("playwright.nodejs.path"); if (nodePath != null) { preinstalledNodePath = Paths.get(nodePath); @@ -87,10 +86,11 @@ private void installBrowsers(Map env) throws IOException, Interr } ProcessBuilder pb = createProcessBuilder(); pb.command().add("install"); + logMessage("Executing: " + String.join(" ", pb.command())); pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); Process p = pb.start(); - boolean result = p.waitFor(10, TimeUnit.MINUTES); + boolean result = p.waitFor(30, TimeUnit.MINUTES); if (!result) { p.destroy(); throw new RuntimeException("Timed out waiting for browsers to install"); diff --git a/playwright/src/test/java/com/microsoft/playwright/TraceViewerFixture.java b/playwright/src/test/java/com/microsoft/playwright/TraceViewerFixture.java new file mode 100644 index 000000000..e69de29bb From 88d9f60cc093597e9d7283fdcc9eb2e086f07f1e Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 26 Aug 2025 00:08:51 +0200 Subject: [PATCH 4/6] fix it --- .../src/test/java/com/microsoft/playwright/TestTracing.java | 1 - 1 file changed, 1 deletion(-) diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java index 2857e5d78..85ab3dead 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java @@ -276,7 +276,6 @@ void traceGroupGroupEnd(@TempDir Path tempDir) throws Exception { Pattern.compile("inner group 1"), Pattern.compile("Click"), Pattern.compile("inner group 2"), - Pattern.compile("Is visible") }); }); } From ed032cf04706ffb7141dfd708eb7e4349d19ee00 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 26 Aug 2025 00:15:09 +0200 Subject: [PATCH 5/6] Review feedback --- .../com/microsoft/playwright/TestTracing.java | 122 ++---------------- .../microsoft/playwright/TraceViewerPage.java | 119 +++++++++++++++++ 2 files changed, 131 insertions(+), 110 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java index 85ab3dead..be9a6fe15 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java @@ -16,8 +16,6 @@ package com.microsoft.playwright; -import com.microsoft.playwright.BrowserType.LaunchOptions; -import com.microsoft.playwright.impl.driver.Driver; import com.microsoft.playwright.options.AriaRole; import com.microsoft.playwright.options.Location; import com.microsoft.playwright.options.MouseButton; @@ -27,7 +25,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -63,7 +60,7 @@ void shouldCollectTrace1(@TempDir Path tempDir) throws Exception { context.tracing().stop(new Tracing.StopOptions().setPath(traceFile)); assertTrue(Files.exists(traceFile)); - showTraceViewer(traceFile, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile, traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Navigate to \"/empty.html\""), Pattern.compile("Set content"), @@ -93,7 +90,7 @@ void shouldCollectTwoTraces(@TempDir Path tempDir) throws Exception { assertTrue(Files.exists(traceFile1)); assertTrue(Files.exists(traceFile2)); - showTraceViewer(traceFile1, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Navigate to \"/empty.html\""), Pattern.compile("Set content"), @@ -101,7 +98,7 @@ void shouldCollectTwoTraces(@TempDir Path tempDir) throws Exception { }); }); - showTraceViewer(traceFile2, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile2, traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Double click"), Pattern.compile("Close") @@ -128,7 +125,7 @@ void shouldWorkWithMultipleChunks(@TempDir Path tempDir) throws Exception { assertTrue(Files.exists(traceFile1)); assertTrue(Files.exists(traceFile2)); - showTraceViewer(traceFile1, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Set content"), Pattern.compile("Click") @@ -138,7 +135,7 @@ void shouldWorkWithMultipleChunks(@TempDir Path tempDir) throws Exception { assertThat(frame.locator("button")).hasText("Click"); }); - showTraceViewer(traceFile2, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile2, traceViewer -> { assertThat(traceViewer.actionTitles()).containsText(new String[] {"Hover"}); FrameLocator frame = traceViewer.snapshotFrame("Hover", 0, false); assertThat(frame.locator("button")).hasText("Click"); @@ -155,7 +152,7 @@ void shouldCollectSources(@TempDir Path tmpDir) throws Exception { Path trace = tmpDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(trace)); - showTraceViewer(trace, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, trace, traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Navigate to \"/empty.html\""), Pattern.compile("Set content"), @@ -210,7 +207,7 @@ void shouldRespectTracesDirAndName(@TempDir Path tempDir) throws Exception { assertTrue(Files.exists(tracesDir.resolve("name2.trace"))); assertTrue(Files.exists(tracesDir.resolve("name2.network"))); - showTraceViewer(tempDir.resolve("trace1.zip"), traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, tempDir.resolve("trace1.zip"), traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Navigate to \"/one-style.html\"") }); @@ -219,7 +216,7 @@ void shouldRespectTracesDirAndName(@TempDir Path tempDir) throws Exception { assertThat(frame.locator("body")).hasText("hello, world!"); }); - showTraceViewer(tempDir.resolve("trace2.zip"), traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, tempDir.resolve("trace2.zip"), traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Navigate to \"/har.html\"") }); @@ -247,7 +244,7 @@ void canCallTracingGroupGroupEndAtAnyTimeAndAutoClose(@TempDir Path tempDir) thr context.tracing().groupEnd(); context.tracing().groupEnd(); - showTraceViewer(traceFile1, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { assertThat(traceViewer.actionTitles()).containsText(new String[] {"actual", "Navigate to \"/empty.html\""}); }); } @@ -268,7 +265,7 @@ void traceGroupGroupEnd(@TempDir Path tempDir) throws Exception { Path traceFile1 = tempDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); - showTraceViewer(traceFile1, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { traceViewer.expandAction("inner group 1"); assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("outer group"), @@ -313,7 +310,7 @@ void shouldTraceVariousAPIs(@TempDir Path tempDir) throws Exception { Path traceFile1 = tempDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); - showTraceViewer(traceFile1, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Install clock"), Pattern.compile("Set content"), @@ -356,105 +353,10 @@ public void shouldNotRecordNetworkActions(@TempDir Path tempDir) throws Exceptio Path traceFile1 = tempDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); - showTraceViewer(traceFile1, traceViewer -> { + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { Pattern.compile("Navigate to \"/empty.html\"") }); }); } - - private void showTraceViewer(Path tracePath, TraceViewerConsumer callback) throws Exception { - Path driverDir = Driver.ensureDriverInstalled(java.util.Collections.emptyMap(), true).driverDir(); - Path traceViewerPath = driverDir.resolve("package").resolve("lib").resolve("vite").resolve("traceViewer"); - Server traceServer = Server.createHttp(Utils.nextFreePort()); - traceServer.setResourceProvider(path -> { - Path filePath = traceViewerPath.resolve(path.substring(1)); - if (Files.exists(filePath) && !Files.isDirectory(filePath)) { - try { - return Files.newInputStream(filePath); - } catch (IOException e) { - return null; - } - } - return null; - }); - traceServer.setRoute("/trace.zip", exchange -> { - exchange.getResponseHeaders().add("Content-Type", "application/zip"); - exchange.sendResponseHeaders(200, Files.size(tracePath)); - Files.copy(tracePath, exchange.getResponseBody()); - exchange.getResponseBody().close(); - }); - - try (Browser browser = browserType.launch(createLaunchOptions()); - BrowserContext context = browser.newContext()) { - Page page = context.newPage(); - page.navigate(traceServer.PREFIX + "/index.html?trace=" + traceServer.PREFIX + "/trace.zip"); - - TraceViewerPage traceViewer = new TraceViewerPage(page); - callback.accept(traceViewer); - } finally { - traceServer.stop(); - } - } - - @FunctionalInterface - interface TraceViewerConsumer { - void accept(TraceViewerPage traceViewer) throws Exception; - } -} - -class TraceViewerPage { - private final Page page; - - public TraceViewerPage(Page page) { - this.page = page; - } - - public Page page() { - return page; - } - - public Locator actionsTree() { - return page.getByTestId("actions-tree"); - } - - public Locator actionTitles() { - return page.locator(".action-title"); - } - - public Locator stackFrames() { - return this.page.getByRole(AriaRole.LIST, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.LISTITEM); - } - - public void selectAction(String title, int ordinal) { - this.actionsTree().getByTitle(title).nth(ordinal).click(); - } - - public void selectAction(String title) { - selectAction(title, 0); - } - - public void selectSnapshot(String name) { - this.page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName(name)).click(); - } - - public FrameLocator snapshotFrame(String actionName, int ordinal, boolean hasSubframe) { - selectAction(actionName, ordinal); - while (page.frames().size() < (hasSubframe ? 4 : 3)) { - page.waitForTimeout(200); - } - return page.frameLocator("iframe.snapshot-visible[name=snapshot]"); - } - - public FrameLocator snapshotFrame(String actionName, int ordinal) { - return snapshotFrame(actionName, ordinal, false); - } - - public void showSourceTab() { - page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName("Source")).click(); - } - - public void expandAction(String title) { - this.actionsTree().getByRole(AriaRole.TREEITEM, new Locator.GetByRoleOptions().setName(title)).locator(".codicon-chevron-right").click(); - } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java b/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java new file mode 100644 index 000000000..cbad8b576 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java @@ -0,0 +1,119 @@ +/* + * 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; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.microsoft.playwright.impl.driver.Driver; +import com.microsoft.playwright.options.AriaRole; + +public class TraceViewerPage { + private final Page page; + + public TraceViewerPage(Page page) { + this.page = page; + } + + public Page page() { + return page; + } + + public Locator actionsTree() { + return page.getByTestId("actions-tree"); + } + + public Locator actionTitles() { + return page.locator(".action-title"); + } + + public Locator stackFrames() { + return this.page.getByRole(AriaRole.LIST, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.LISTITEM); + } + + public void selectAction(String title, int ordinal) { + this.actionsTree().getByTitle(title).nth(ordinal).click(); + } + + public void selectAction(String title) { + selectAction(title, 0); + } + + public void selectSnapshot(String name) { + this.page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName(name)).click(); + } + + public FrameLocator snapshotFrame(String actionName, int ordinal, boolean hasSubframe) { + selectAction(actionName, ordinal); + while (page.frames().size() < (hasSubframe ? 4 : 3)) { + page.waitForTimeout(200); + } + return page.frameLocator("iframe.snapshot-visible[name=snapshot]"); + } + + public FrameLocator snapshotFrame(String actionName, int ordinal) { + return snapshotFrame(actionName, ordinal, false); + } + + public void showSourceTab() { + page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName("Source")).click(); + } + + public void expandAction(String title) { + this.actionsTree().getByRole(AriaRole.TREEITEM, new Locator.GetByRoleOptions().setName(title)).locator(".codicon-chevron-right").click(); + } + + static void showTraceViewer(BrowserType browserType, Path tracePath, TraceViewerConsumer callback) throws Exception { + Path driverDir = Driver.ensureDriverInstalled(java.util.Collections.emptyMap(), true).driverDir(); + Path traceViewerPath = driverDir.resolve("package").resolve("lib").resolve("vite").resolve("traceViewer"); + Server traceServer = Server.createHttp(Utils.nextFreePort()); + traceServer.setResourceProvider(path -> { + Path filePath = traceViewerPath.resolve(path.substring(1)); + if (Files.exists(filePath) && !Files.isDirectory(filePath)) { + try { + return Files.newInputStream(filePath); + } catch (IOException e) { + return null; + } + } + return null; + }); + traceServer.setRoute("/trace.zip", exchange -> { + exchange.getResponseHeaders().add("Content-Type", "application/zip"); + exchange.sendResponseHeaders(200, Files.size(tracePath)); + Files.copy(tracePath, exchange.getResponseBody()); + exchange.getResponseBody().close(); + }); + + try (Browser browser = browserType.launch(TestBase.createLaunchOptions()); + BrowserContext context = browser.newContext()) { + Page page = context.newPage(); + page.navigate(traceServer.PREFIX + "/index.html?trace=" + traceServer.PREFIX + "/trace.zip"); + + TraceViewerPage traceViewer = new TraceViewerPage(page); + callback.accept(traceViewer); + } finally { + traceServer.stop(); + } + } + + @FunctionalInterface + interface TraceViewerConsumer { + void accept(TraceViewerPage traceViewer) throws Exception; + } +} From 21caa826cd6ed2f2da9c964a703a8635644d2b9f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 26 Aug 2025 10:01:17 +0200 Subject: [PATCH 6/6] Review feedback --- .../playwright/impl/driver/jar/DriverJar.java | 4 +-- .../playwright/TraceViewerFixture.java | 0 .../microsoft/playwright/TraceViewerPage.java | 26 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) delete mode 100644 playwright/src/test/java/com/microsoft/playwright/TraceViewerFixture.java diff --git a/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java b/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java index ab2d221c0..654ff6732 100644 --- a/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java +++ b/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java @@ -40,6 +40,7 @@ public DriverJar() throws IOException { driverTempDir = alternativeTmpdir == null ? Files.createTempDirectory(prefix) : Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix); + driverTempDir.toFile().deleteOnExit(); String nodePath = System.getProperty("playwright.nodejs.path"); if (nodePath != null) { preinstalledNodePath = Paths.get(nodePath); @@ -86,11 +87,10 @@ private void installBrowsers(Map env) throws IOException, Interr } ProcessBuilder pb = createProcessBuilder(); pb.command().add("install"); - logMessage("Executing: " + String.join(" ", pb.command())); pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); Process p = pb.start(); - boolean result = p.waitFor(30, TimeUnit.MINUTES); + boolean result = p.waitFor(10, TimeUnit.MINUTES); if (!result) { p.destroy(); throw new RuntimeException("Timed out waiting for browsers to install"); diff --git a/playwright/src/test/java/com/microsoft/playwright/TraceViewerFixture.java b/playwright/src/test/java/com/microsoft/playwright/TraceViewerFixture.java deleted file mode 100644 index e69de29bb..000000000 diff --git a/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java b/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java index cbad8b576..b64079386 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java +++ b/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java @@ -23,42 +23,42 @@ import com.microsoft.playwright.impl.driver.Driver; import com.microsoft.playwright.options.AriaRole; -public class TraceViewerPage { +class TraceViewerPage { private final Page page; - public TraceViewerPage(Page page) { + TraceViewerPage(Page page) { this.page = page; } - public Page page() { + Page page() { return page; } - public Locator actionsTree() { + Locator actionsTree() { return page.getByTestId("actions-tree"); } - public Locator actionTitles() { + Locator actionTitles() { return page.locator(".action-title"); } - public Locator stackFrames() { + Locator stackFrames() { return this.page.getByRole(AriaRole.LIST, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.LISTITEM); } - public void selectAction(String title, int ordinal) { + void selectAction(String title, int ordinal) { this.actionsTree().getByTitle(title).nth(ordinal).click(); } - public void selectAction(String title) { + void selectAction(String title) { selectAction(title, 0); } - public void selectSnapshot(String name) { + void selectSnapshot(String name) { this.page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName(name)).click(); } - public FrameLocator snapshotFrame(String actionName, int ordinal, boolean hasSubframe) { + FrameLocator snapshotFrame(String actionName, int ordinal, boolean hasSubframe) { selectAction(actionName, ordinal); while (page.frames().size() < (hasSubframe ? 4 : 3)) { page.waitForTimeout(200); @@ -66,15 +66,15 @@ public FrameLocator snapshotFrame(String actionName, int ordinal, boolean hasSub return page.frameLocator("iframe.snapshot-visible[name=snapshot]"); } - public FrameLocator snapshotFrame(String actionName, int ordinal) { + FrameLocator snapshotFrame(String actionName, int ordinal) { return snapshotFrame(actionName, ordinal, false); } - public void showSourceTab() { + void showSourceTab() { page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName("Source")).click(); } - public void expandAction(String title) { + void expandAction(String title) { this.actionsTree().getByRole(AriaRole.TREEITEM, new Locator.GetByRoleOptions().setName(title)).locator(".codicon-chevron-right").click(); }