From 2a6cdff6646bb614720c7d299331b950a162b7bc Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 26 Aug 2025 10:43:18 +0200 Subject: [PATCH] chore: migrate Trace Viewer tests to use real Trace viewer (#1830) --- .../java/com/microsoft/playwright/Server.java | 16 +- .../com/microsoft/playwright/TestTracing.java | 235 +++++++++++------- .../microsoft/playwright/TraceViewerPage.java | 119 +++++++++ 3 files changed, 277 insertions(+), 93 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java diff --git a/playwright/src/test/java/com/microsoft/playwright/Server.java b/playwright/src/test/java/com/microsoft/playwright/Server.java index beeea69d..c8b6838a 100644 --- a/playwright/src/test/java/com/microsoft/playwright/Server.java +++ b/playwright/src/test/java/com/microsoft/playwright/Server.java @@ -23,6 +23,7 @@ import java.net.InetSocketAddress; 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; @@ -40,6 +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 Function resourceProvider; private static class Auth { public final String user; @@ -75,6 +77,8 @@ public class Server implements HttpHandler { 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() { @@ -93,6 +97,10 @@ public class Server implements HttpHandler { gzipRoutes.add(path); } + void setResourceProvider(Function resourceProvider) { + this.resourceProvider = resourceProvider; + } + static class Request { public final String url; public final String method; @@ -187,18 +195,16 @@ public class Server implements HttpHandler { path = "/index.html"; } - // 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 4b22abd2..be9a6fe1 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.google.gson.Gson; -import com.google.gson.annotations.SerializedName; import com.microsoft.playwright.options.AriaRole; import com.microsoft.playwright.options.Location; import com.microsoft.playwright.options.MouseButton; @@ -27,18 +25,12 @@ import org.junit.jupiter.api.BeforeAll; 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; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.regex.Pattern; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Arrays.asList; +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import static org.junit.jupiter.api.Assertions.*; public class TestTracing extends TestBase { @@ -57,7 +49,7 @@ public class TestTracing extends TestBase { } @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 +60,18 @@ public class TestTracing extends TestBase { context.tracing().stop(new Tracing.StopOptions().setPath(traceFile)); assertTrue(Files.exists(traceFile)); + TraceViewerPage.showTraceViewer(this.browserType, 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 +89,25 @@ public class TestTracing extends TestBase { assertTrue(Files.exists(traceFile1)); assertTrue(Files.exists(traceFile2)); + + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Navigate to \"/empty.html\""), + Pattern.compile("Set content"), + Pattern.compile("Click") + }); + }); + + TraceViewerPage.showTraceViewer(this.browserType, 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 +124,60 @@ public class TestTracing extends TestBase { assertTrue(Files.exists(traceFile1)); assertTrue(Files.exists(traceFile2)); + + TraceViewerPage.showTraceViewer(this.browserType, 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"); + }); + + 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"); + }); } @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()); + TraceViewerPage.showTraceViewer(this.browserType, 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(\"\");"); + }); + } - 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 myMethodOuter() { + myMethodInner(); + } + + private void myMethodInner() { + page.getByText("Click").click(); } @Test @@ -140,7 +187,7 @@ public class TestTracing extends TestBase { } @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 +206,24 @@ public class TestTracing extends TestBase { 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"))); + + TraceViewerPage.showTraceViewer(this.browserType, 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!"); + }); + + TraceViewerPage.showTraceViewer(this.browserType, 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 +244,9 @@ public class TestTracing extends TestBase { 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); - + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { + assertThat(traceViewer.actionTitles()).containsText(new String[] {"actual", "Navigate to \"/empty.html\""}); + }); } @Test @@ -202,9 +265,16 @@ public class TestTracing extends TestBase { 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); + TraceViewerPage.showTraceViewer(this.browserType, 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"), + }); + }); } @Test @@ -240,64 +310,53 @@ public class TestTracing extends TestBase { 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); + TraceViewerPage.showTraceViewer(this.browserType, 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") + }); + }); } - private static class TraceEvent { - String type; - String name; - String title; - @SerializedName("class") - String clazz; - String method; - Double startTime; - Double endTime; - String callId; + @Test + public void shouldNotRecordNetworkActions(@TempDir Path tempDir) throws Exception { + context.tracing().start(new Tracing.StartOptions()); - String renderedTitle() { - if (title != null) { - return title; - } - if (clazz != null && method != null) { - return clazz + "." + method; - } - return null; - } - } + page.onRequest(request -> { + request.allHeaders(); + }); + page.onResponse(response -> { + response.text(); + }); + page.navigate(server.EMPTY_PAGE); - 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()); + Path traceFile1 = tempDir.resolve("trace1.zip"); + context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); + + TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> { + assertThat(traceViewer.actionTitles()).hasText(new Pattern[] { + Pattern.compile("Navigate to \"/empty.html\"") + }); + }); } } 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 00000000..b6407938 --- /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; + +class TraceViewerPage { + private final Page page; + + TraceViewerPage(Page page) { + this.page = page; + } + + Page page() { + return page; + } + + Locator actionsTree() { + return page.getByTestId("actions-tree"); + } + + Locator actionTitles() { + return page.locator(".action-title"); + } + + Locator stackFrames() { + return this.page.getByRole(AriaRole.LIST, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.LISTITEM); + } + + void selectAction(String title, int ordinal) { + this.actionsTree().getByTitle(title).nth(ordinal).click(); + } + + void selectAction(String title) { + selectAction(title, 0); + } + + void selectSnapshot(String name) { + this.page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName(name)).click(); + } + + 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]"); + } + + FrameLocator snapshotFrame(String actionName, int ordinal) { + return snapshotFrame(actionName, ordinal, false); + } + + void showSourceTab() { + page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName("Source")).click(); + } + + 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; + } +}