Skip to content

Commit 86e9159

Browse files
authored
feat: include source files in trace (microsoft#754)
1 parent 963afac commit 86e9159

11 files changed

Lines changed: 179 additions & 63 deletions

File tree

.github/workflows/test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ jobs:
4040
run: mvn test --no-transfer-progress --fail-at-end
4141
env:
4242
BROWSER: ${{ matrix.browser }}
43+
- name: Run tracing tests w/ sources
44+
run: mvn test --no-transfer-progress --fail-at-end -D test=*TestTracing*
45+
env:
46+
BROWSER: ${{ matrix.browser }}
47+
PLAYWRIGHT_JAVA_SRC: src/test/java
4348
- name: Test Spring Boot Starter
4449
shell: bash
4550
env:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
1313
| :--- | :---: | :---: | :---: |
1414
| Chromium <!-- GEN:chromium-version -->99.0.4763.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
1515
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> ||||
16-
| Firefox <!-- GEN:firefox-version -->94.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
16+
| Firefox <!-- GEN:firefox-version -->95.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
1717

1818
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.
1919

assertions/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
/**
2424
* The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page} state in the
25-
* tests. A new instance of {@code LocatorAssertions} is created by calling {@link PlaywrightAssertions#assertThat
25+
* tests. A new instance of {@code PageAssertions} is created by calling {@link PlaywrightAssertions#assertThat
2626
* PlaywrightAssertions.assertThat()}:
2727
* <pre>{@code
2828
* ...

playwright/src/main/java/com/microsoft/playwright/Tracing.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ class StartOptions {
5151
* Whether to capture DOM snapshot on every action.
5252
*/
5353
public Boolean snapshots;
54+
/**
55+
* Whether to include source files for trace actions. List of the directories with source code for the application must be
56+
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable.
57+
*/
58+
public Boolean sources;
5459
/**
5560
* Trace name to be shown in the Trace Viewer.
5661
*/
@@ -78,6 +83,14 @@ public StartOptions setSnapshots(boolean snapshots) {
7883
this.snapshots = snapshots;
7984
return this;
8085
}
86+
/**
87+
* Whether to include source files for trace actions. List of the directories with source code for the application must be
88+
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable.
89+
*/
90+
public StartOptions setSources(boolean sources) {
91+
this.sources = sources;
92+
return this;
93+
}
8194
/**
8295
* Trace name to be shown in the Trace Viewer.
8396
*/

playwright/src/main/java/com/microsoft/playwright/impl/Connection.java

Lines changed: 8 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,18 @@
1616
package com.microsoft.playwright.impl;
1717

1818
import com.google.gson.Gson;
19-
import com.google.gson.JsonArray;
2019
import com.google.gson.JsonElement;
2120
import com.google.gson.JsonObject;
2221
import com.microsoft.playwright.Playwright;
2322
import com.microsoft.playwright.PlaywrightException;
2423
import com.microsoft.playwright.TimeoutError;
2524

26-
import java.io.File;
2725
import java.io.IOException;
28-
import java.nio.file.Files;
29-
import java.nio.file.Path;
3026
import java.nio.file.Paths;
3127
import java.time.Duration;
3228
import java.util.HashMap;
3329
import java.util.Map;
3430

35-
import static com.microsoft.playwright.impl.LoggingSupport.logWithTimestamp;
3631
import static com.microsoft.playwright.impl.Serialization.gson;
3732

3833
class Message {
@@ -62,7 +57,7 @@ public class Connection {
6257
private final Map<String, ChannelOwner> objects = new HashMap<>();
6358
private final Root root;
6459
private int lastId = 0;
65-
private final Path srcDir;
60+
private final StackTraceCollector stackTraceCollector;
6661
private final Map<Integer, WaitableResult<JsonElement>> callbacks = new HashMap<>();
6762
private String apiName;
6863
private static final boolean isLogging;
@@ -91,14 +86,11 @@ Playwright initialize() {
9186
this.transport = transport;
9287
root = new Root(this);
9388
String srcRoot = System.getenv("PLAYWRIGHT_JAVA_SRC");
94-
if (srcRoot == null) {
95-
srcDir = null;
96-
} else {
97-
srcDir = Paths.get(srcRoot);
98-
if (!Files.exists(srcDir)) {
99-
throw new PlaywrightException("PLAYWRIGHT_JAVA_SRC environment variable points to non-existing location: '" + srcRoot + "'");
100-
}
101-
}
89+
stackTraceCollector = (srcRoot == null) ? null: new StackTraceCollector(Paths.get(srcRoot));
90+
}
91+
92+
boolean isCollectingStacks() {
93+
return stackTraceCollector != null;
10294
}
10395

10496
String setApiName(String name) {
@@ -119,45 +111,6 @@ public WaitableResult<JsonElement> sendMessageAsync(String guid, String method,
119111
return internalSendMessage(guid, method, params);
120112
}
121113

122-
private String sourceFile(StackTraceElement frame) {
123-
String pkg = frame.getClassName();
124-
int lastDot = pkg.lastIndexOf('.');
125-
if (lastDot == -1) {
126-
pkg = "";
127-
} else {
128-
pkg = frame.getClassName().substring(0, lastDot + 1);
129-
}
130-
pkg = pkg.replace('.', File.separatorChar);
131-
return srcDir.resolve(pkg).resolve(frame.getFileName()).toString();
132-
}
133-
134-
private JsonArray currentStackTrace() {
135-
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
136-
137-
int index = 0;
138-
while (index < stack.length && !stack[index].getClassName().equals(getClass().getName())) {
139-
index++;
140-
};
141-
// Find Playwright API call
142-
while (index < stack.length && stack[index].getClassName().startsWith("com.microsoft.playwright.")) {
143-
// hack for tests
144-
if (stack[index].getClassName().startsWith("com.microsoft.playwright.Test")) {
145-
break;
146-
}
147-
index++;
148-
}
149-
JsonArray jsonStack = new JsonArray();
150-
for (; index < stack.length; index++) {
151-
StackTraceElement frame = stack[index];
152-
JsonObject jsonFrame = new JsonObject();
153-
jsonFrame.addProperty("file", sourceFile(frame));
154-
jsonFrame.addProperty("line", frame.getLineNumber());
155-
jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
156-
jsonStack.add(jsonFrame);
157-
}
158-
return jsonStack;
159-
}
160-
161114
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params) {
162115
int id = ++lastId;
163116
WaitableResult<JsonElement> result = new WaitableResult<>();
@@ -168,8 +121,8 @@ private WaitableResult<JsonElement> internalSendMessage(String guid, String meth
168121
message.addProperty("method", method);
169122
message.add("params", params);
170123
JsonObject metadata = new JsonObject();
171-
if (srcDir != null) {
172-
metadata.add("stack", currentStackTrace());
124+
if (stackTraceCollector != null) {
125+
metadata.add("stack", stackTraceCollector.currentStackTrace());
173126
}
174127
if (apiName != null) {
175128
metadata.addProperty("apiName", apiName);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.playwright.impl;
18+
19+
import com.google.gson.JsonArray;
20+
import com.google.gson.JsonObject;
21+
import com.microsoft.playwright.PlaywrightException;
22+
23+
import java.io.File;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
27+
class StackTraceCollector {
28+
private final Path srcDir;
29+
30+
StackTraceCollector(Path srcDir) {
31+
if (!Files.exists(srcDir.toAbsolutePath())) {
32+
throw new PlaywrightException("Source location doesn't exist: '" + srcDir.toAbsolutePath() + "'");
33+
}
34+
this.srcDir = srcDir;
35+
}
36+
37+
private String sourceFile(StackTraceElement frame) {
38+
String pkg = frame.getClassName();
39+
int lastDot = pkg.lastIndexOf('.');
40+
if (lastDot == -1) {
41+
pkg = "";
42+
} else {
43+
pkg = frame.getClassName().substring(0, lastDot + 1);
44+
}
45+
pkg = pkg.replace('.', File.separatorChar);
46+
String file = frame.getFileName();
47+
if (file == null) {
48+
return "";
49+
}
50+
return srcDir.resolve(pkg).resolve(file).toString();
51+
}
52+
53+
JsonArray currentStackTrace() {
54+
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
55+
56+
int index = 0;
57+
while (index < stack.length && !stack[index].getClassName().equals(getClass().getName())) {
58+
index++;
59+
};
60+
// Find Playwright API call
61+
while (index < stack.length && stack[index].getClassName().startsWith("com.microsoft.playwright.")) {
62+
// hack for tests
63+
if (stack[index].getClassName().startsWith("com.microsoft.playwright.Test")) {
64+
break;
65+
}
66+
index++;
67+
}
68+
JsonArray jsonStack = new JsonArray();
69+
for (; index < stack.length; index++) {
70+
StackTraceElement frame = stack[index];
71+
JsonObject jsonFrame = new JsonObject();
72+
jsonFrame.addProperty("file", sourceFile(frame));
73+
jsonFrame.addProperty("line", frame.getLineNumber());
74+
jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
75+
jsonStack.add(jsonFrame);
76+
}
77+
return jsonStack;
78+
}
79+
80+
81+
}

playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package com.microsoft.playwright.impl;
1818

19-
import com.google.gson.JsonElement;
2019
import com.google.gson.JsonObject;
20+
import com.microsoft.playwright.PlaywrightException;
2121
import com.microsoft.playwright.Tracing;
2222

2323
import java.nio.file.Path;
@@ -26,16 +26,23 @@
2626

2727
class TracingImpl implements Tracing {
2828
private final BrowserContextImpl context;
29+
private boolean includeSources;
2930

3031
TracingImpl(BrowserContextImpl context) {
3132
this.context = context;
3233
}
3334

3435
private void stopChunkImpl(Path path) {
36+
boolean isRemote = context.browser() != null && context.browser().isRemote;
3537
JsonObject params = new JsonObject();
3638
String mode = "doNotSave";
3739
if (path != null) {
38-
mode = "compressTrace";
40+
if (isRemote) {
41+
mode = "compressTrace";
42+
} else {
43+
// TODO: support source zips and do compression on the client.
44+
mode = "compressTraceAndSources";
45+
}
3946
}
4047
params.addProperty("mode", mode);
4148
JsonObject json = context.sendMessage("tracingStopChunk", params).getAsJsonObject();
@@ -45,7 +52,7 @@ private void stopChunkImpl(Path path) {
4552
ArtifactImpl artifact = context.connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
4653
// In case of CDP connection browser is null but since the connection is established by
4754
// the driver it is safe to consider the artifact local.
48-
if (context.browser() != null && context.browser().isRemote) {
55+
if (isRemote) {
4956
artifact.isRemote = true;
5057
}
5158
artifact.saveAs(path);
@@ -77,6 +84,13 @@ private void startImpl(StartOptions options) {
7784
options = new StartOptions();
7885
}
7986
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
87+
includeSources = options.sources != null;
88+
if (includeSources) {
89+
if (!context.connection.isCollectingStacks()) {
90+
throw new PlaywrightException("Source root directories must be provided to enable source collection");
91+
}
92+
params.addProperty("sources", true);
93+
}
8094
context.sendMessage("tracingStart", params);
8195
context.sendMessage("tracingStartChunk");
8296
}

playwright/src/test/java/com/microsoft/playwright/TestClick.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.junit.jupiter.api.condition.DisabledIf;
2121
import org.junit.jupiter.api.condition.EnabledIf;
2222

23+
import java.nio.file.Paths;
2324
import java.util.ArrayList;
2425
import java.util.List;
2526

playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ void shouldWork() {
4545

4646
@Test
4747
void shouldHandleNestedFrames() {
48-
page.setViewportSize(500, 500);
48+
page.setViewportSize(616, 500);
4949
page.navigate(server.PREFIX + "/frames/nested-frames.html");
5050
Frame nestedFrame = page.frame("dos");
5151
assertNotNull(nestedFrame);

playwright/src/test/java/com/microsoft/playwright/TestTracing.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,31 @@
1616

1717
package com.microsoft.playwright;
1818

19+
import org.junit.jupiter.api.Assumptions;
1920
import org.junit.jupiter.api.BeforeAll;
2021
import org.junit.jupiter.api.Test;
2122
import org.junit.jupiter.api.io.TempDir;
2223

24+
import java.io.*;
25+
import java.nio.charset.StandardCharsets;
2326
import java.nio.file.Files;
2427
import java.nio.file.Path;
25-
28+
import java.nio.file.Paths;
29+
import java.util.*;
30+
import java.util.stream.Collectors;
31+
import java.util.zip.GZIPOutputStream;
32+
import java.util.zip.ZipEntry;
33+
import java.util.zip.ZipInputStream;
34+
35+
import static com.microsoft.playwright.Utils.copy;
36+
import static java.nio.charset.StandardCharsets.UTF_8;
37+
import static org.junit.jupiter.api.Assertions.assertEquals;
2638
import static org.junit.jupiter.api.Assertions.assertTrue;
2739

2840
public class TestTracing extends TestBase {
2941

42+
private static final String _PW_JAVA_TEST_SRC = "_PW_JAVA_TEST_SRC";
43+
3044
@Override
3145
@BeforeAll
3246
void launchBrowser() {
@@ -96,4 +110,39 @@ void shouldWorkWithMultipleChunks(@TempDir Path tempDir) {
96110
assertTrue(Files.exists(traceFile1));
97111
assertTrue(Files.exists(traceFile2));
98112
}
113+
114+
@Test
115+
void shouldCollectSources(@TempDir Path tmpDir) throws IOException {
116+
Assumptions.assumeTrue(System.getenv("PLAYWRIGHT_JAVA_SRC") != null, "PLAYWRIGHT_JAVA_SRC must point to the directory containing this test source.");
117+
context.tracing().start(new Tracing.StartOptions().setSources(true));
118+
page.navigate(server.EMPTY_PAGE);
119+
page.setContent("<button>Click</button>");
120+
page.click("'Click'");
121+
Path trace = tmpDir.resolve("trace1.zip");
122+
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
123+
124+
Map<String, byte[]> entries = parseTrace(trace);
125+
Map<String, byte[]> sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
126+
assertEquals(1, sources.size());
127+
128+
String path = getClass().getName().replace('.', File.separatorChar);
129+
Path sourceFile = Paths.get(System.getenv("PLAYWRIGHT_JAVA_SRC"), path + ".java");
130+
byte[] thisFile = Files.readAllBytes(sourceFile);
131+
assertEquals(new String(thisFile, UTF_8), new String(sources.values().iterator().next(), UTF_8));
132+
}
133+
134+
private static Map<String, byte[]> parseTrace(Path trace) throws IOException {
135+
Map<String, byte[]> entries = new HashMap<>();
136+
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(trace.toFile()))) {
137+
for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) {
138+
ByteArrayOutputStream content = new ByteArrayOutputStream();
139+
try (OutputStream output = content) {
140+
copy(zis, output);
141+
}
142+
entries.put(zipEntry.getName(), content.toByteArray());
143+
}
144+
zis.closeEntry();
145+
}
146+
return entries;
147+
}
99148
}

0 commit comments

Comments
 (0)