Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions playwright/src/main/java/com/microsoft/playwright/Coverage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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.util.List;

/**
* Coverage gathers information about parts of JavaScript and CSS that were used by the page.
*
* <p> Coverage APIs are only supported on Chromium-based browsers.
*/
public interface Coverage {
class StartJSCoverageOptions {
/**
* Whether anonymous scripts generated by the page should be reported. Defaults to {@code false}.
*/
public Boolean reportAnonymousScripts;
/**
* Whether to reset coverage on every navigation. Defaults to {@code true}.
*/
public Boolean resetOnNavigation;

public StartJSCoverageOptions setReportAnonymousScripts(boolean reportAnonymousScripts) {
this.reportAnonymousScripts = reportAnonymousScripts;
return this;
}

public StartJSCoverageOptions setResetOnNavigation(boolean resetOnNavigation) {
this.resetOnNavigation = resetOnNavigation;
return this;
}
}

class StartCSSCoverageOptions {
/**
* Whether to reset coverage on every navigation. Defaults to {@code true}.
*/
public Boolean resetOnNavigation;

public StartCSSCoverageOptions setResetOnNavigation(boolean resetOnNavigation) {
this.resetOnNavigation = resetOnNavigation;
return this;
}
}

class ScriptCoverage {
/** Script URL. */
public String url;
/** Script ID. */
public String scriptId;
/** Script content, if applicable. */
public String source;
/** V8-specific function coverage. */
public List<FunctionCoverage> functions;
}

class FunctionCoverage {
public String functionName;
public boolean isBlockCoverage;
public List<FunctionCoverageRange> ranges;
}

class FunctionCoverageRange {
public int startOffset;
public int endOffset;
public int count;
}

class StyleSheetCoverage {
/** StyleSheet URL. */
public String url;
/** StyleSheet content, if available. */
public String text;
/** StyleSheet ranges that were used, sorted and non-overlapping. */
public List<StyleSheetCoverageRange> ranges;
}

class StyleSheetCoverageRange {
public int start;
public int end;
}

/**
* Starts JavaScript coverage with default options.
*/
default void startJSCoverage() {
startJSCoverage(null);
}

/**
* Starts JavaScript coverage.
*
* <p> Anonymous scripts do not have an associated URL and are created dynamically using {@code eval} or {@code new
* Function}. When {@link StartJSCoverageOptions#setReportAnonymousScripts(boolean)} is enabled, anonymous scripts are
* reported with an empty URL.
*/
void startJSCoverage(StartJSCoverageOptions options);

/**
* Stops JavaScript coverage and returns reports for all scripts.
*/
List<ScriptCoverage> stopJSCoverage();

/**
* Starts CSS coverage with default options.
*/
default void startCSSCoverage() {
startCSSCoverage(null);
}

/**
* Starts CSS coverage.
*/
void startCSSCoverage(StartCSSCoverageOptions options);

/**
* Stops CSS coverage and returns reports for all stylesheets.
*
* <p> CSS coverage does not include dynamically injected style tags without source URLs.
*/
List<StyleSheetCoverage> stopCSSCoverage();
}
7 changes: 6 additions & 1 deletion playwright/src/main/java/com/microsoft/playwright/Page.java
Original file line number Diff line number Diff line change
Expand Up @@ -3874,6 +3874,12 @@ public WaitForWorkerOptions setTimeout(double timeout) {
* @since v1.45
*/
Clock clock();
/**
* Browser-specific coverage implementation.
*
* <p> Coverage APIs are only supported on Chromium-based browsers.
*/
Coverage coverage();
/**
* Adds a script which would be evaluated in one of the following scenarios:
* <ul>
Expand Down Expand Up @@ -8721,4 +8727,3 @@ default Worker waitForWorker(Runnable callback) {
*/
List<Worker> workers();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.JsonObject;
import com.microsoft.playwright.Coverage;

import java.util.Arrays;
import java.util.List;

import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson;

class CoverageImpl implements Coverage {
private final PageImpl page;

CoverageImpl(PageImpl page) {
this.page = page;
}

@Override
public void startJSCoverage(StartJSCoverageOptions options) {
JsonObject params = gson().toJsonTree(options == null ? new StartJSCoverageOptions() : options).getAsJsonObject();
page.sendMessage("startJSCoverage", params, NO_TIMEOUT);
}

@Override
public List<ScriptCoverage> stopJSCoverage() {
JsonObject result = page.sendMessage("stopJSCoverage", new JsonObject(), NO_TIMEOUT).getAsJsonObject();
return Arrays.asList(gson().fromJson(result.getAsJsonArray("entries"), ScriptCoverage[].class));
}

@Override
public void startCSSCoverage(StartCSSCoverageOptions options) {
JsonObject params = gson().toJsonTree(options == null ? new StartCSSCoverageOptions() : options).getAsJsonObject();
page.sendMessage("startCSSCoverage", params, NO_TIMEOUT);
}

@Override
public List<StyleSheetCoverage> stopCSSCoverage() {
JsonObject result = page.sendMessage("stopCSSCoverage", new JsonObject(), NO_TIMEOUT).getAsJsonObject();
return Arrays.asList(gson().fromJson(result.getAsJsonArray("entries"), StyleSheetCoverage[].class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class PageImpl extends ChannelOwner implements Page {
private final MouseImpl mouse;
private final TouchscreenImpl touchscreen;
private final ScreencastImpl screencast;
private final CoverageImpl coverage;
private final WebStorageImpl localStorage;
private final WebStorageImpl sessionStorage;
final Waitable<?> waitableClosedOrCrashed;
Expand Down Expand Up @@ -139,6 +140,7 @@ enum EventType {
mouse = new MouseImpl(this);
touchscreen = new TouchscreenImpl(this);
screencast = new ScreencastImpl(this);
coverage = new CoverageImpl(this);
localStorage = new WebStorageImpl(this, "local");
sessionStorage = new WebStorageImpl(this, "session");
frames.add(mainFrame);
Expand Down Expand Up @@ -457,6 +459,11 @@ public ClockImpl clock() {
return browserContext.clock();
}

@Override
public Coverage coverage() {
return coverage;
}

@Override
public Page waitForClose(WaitForCloseOptions options, Runnable code) {
return withWaitLogging("Page.waitForClose", logger -> waitForCloseImpl(options, code));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

public class TestCoverage extends TestBase {
@BeforeEach
void onlyChromium() {
assumeTrue(isChromium());
}

@Test
void shouldCollectJavaScriptCoverage() {
page.coverage().startJSCoverage();
page.evaluate("() => eval('function foo() { return 42; } foo(); //# sourceURL=nice-name.js')");
List<Coverage.ScriptCoverage> coverage = page.coverage().stopJSCoverage();

assertEquals(1, coverage.size());
assertEquals("nice-name.js", coverage.get(0).url);
Coverage.FunctionCoverage function = coverage.get(0).functions.stream()
.filter(entry -> "foo".equals(entry.functionName))
.findFirst()
.orElseThrow(AssertionError::new);
assertEquals(1, function.ranges.get(0).count);
}

@Test
void shouldReportAnonymousScriptsWhenEnabled() {
page.coverage().startJSCoverage(new Coverage.StartJSCoverageOptions().setReportAnonymousScripts(true));
page.evaluate("() => eval('2 + 2')");
List<Coverage.ScriptCoverage> coverage = page.coverage().stopJSCoverage();

assertTrue(coverage.stream().anyMatch(entry -> "2 + 2".equals(entry.source)));
}

@Test
void shouldResetJavaScriptCoverageOnNavigation() {
page.coverage().startJSCoverage();
page.evaluate("() => eval('2 + 2 //# sourceURL=before-navigation.js')");
page.navigate(server.EMPTY_PAGE);

assertTrue(page.coverage().stopJSCoverage().isEmpty());
}

@Test
void shouldCollectCSSCoverage() {
page.coverage().startCSSCoverage();
page.setContent("<style>div { color: green; } span { color: red; }</style><div>hello</div>");
List<Coverage.StyleSheetCoverage> coverage = page.coverage().stopCSSCoverage();

assertEquals(1, coverage.size());
assertFalse(coverage.get(0).ranges.isEmpty());
Coverage.StyleSheetCoverageRange range = coverage.get(0).ranges.get(0);
assertEquals("div { color: green; }", coverage.get(0).text.substring(range.start, range.end));
}
}