Skip to content
Merged
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
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,53 @@ Each driver is assembled from the [`playwright-core`](https://www.npmjs.com/pack

```bash
mvn compile
# Runs every test in Chromium, Firefox and WebKit
mvn test
# Selecting browsers
BROWSER=chromium mvn test
BROWSER=firefox,webkit mvn test
# Executing a single test
BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize
# Executing a single test class
BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes
```

Set `HEADED=1` to watch the browsers and `BROWSER_CHANNEL=chrome` (or `msedge`, ...) to test a stable channel.

### Writing tests

Tests use the fixtures from `playwright/src/test/java/com/microsoft/playwright/fixtures`, modeled after the
upstream Playwright Test fixtures. A test is a `@PlaywrightTest` method that declares the fixtures it needs
as parameters; it runs once per selected browser, in a fresh context:

```java
@PlaywrightTest
void shouldWork(Page page, Server server) {
page.navigate(server.EMPTY_PAGE);
}
```

The built-in fixtures are `Playwright`, `BrowserName`, `BrowserType`, `BrowserType.LaunchOptions`, `Browser`,
`Browser.NewContextOptions`, `BrowserContext`, `Page`, `Server` and `HttpsServer`. Playwright, the browser and
the test servers are shared by the tests that run one after another on the same thread; the context and the
page are created for each test and closed afterwards. A test class adds or overrides fixtures with
`@Fixture` methods, whose parameters are the fixtures they depend on:

```java
@Fixture
Browser.NewContextOptions contextOptions() {
return new Browser.NewContextOptions().setHasTouch(true);
}

@Fixture(scope = Scope.WORKER)
static BrowserType.LaunchOptions launchOptions(BrowserType.LaunchOptions defaults) {
return defaults.setArgs(asList("--enable-features=WebMCP"));
}
```

Use `@DisabledOnBrowser` / `@EnabledOnBrowser` for browser-specific tests, and a `@Nested @ParameterizedClass`
with a `@Fixture` method reading its `@Parameter` field to run the same tests with different options.

### Generating API

Public Java API is generated from api.json, which is generated from the upstream Playwright source at the exact commit that produced the driver version in [scripts/DRIVER_VERSION](scripts/DRIVER_VERSION) (resolved via `npm view playwright@<version> gitHead`). `scripts/generate_api.sh` fetches a minimal upstream checkout automatically; set `PW_SRC_DIR` to reuse an existing `microsoft/playwright` checkout instead. To regenerate Java interfaces for the current driver run:
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

import com.microsoft.playwright.impl.driver.Driver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.Collections;

import static com.microsoft.playwright.fixtures.Env.isWindows;
import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* A {@code playwright launch-server} process that tests connect to, the counterpart of the
* upstream {@code startRemoteServer} fixture.
*/
class BrowserServer implements AutoCloseable {
final Process process;
final String wsEndpoint;

static BrowserServer launch(BrowserType browserType) {
try {
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
Path dir = driver.driverDir();
String node = dir.resolve(isWindows ? "node.exe" : "node").toString();
String cliJs = dir.resolve("package/cli.js").toString();
// We launch node process directly instead of using playwright.sh script as killing the script
// process will leave node process running and killing it would be more hassle.
ProcessBuilder pb = new ProcessBuilder(node, cliJs, "launch-server", "--browser", browserType.name());
pb.directory(dir.toFile());
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = pb.start();
BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()));
String wsEndpoint = input.readLine();
if (!wsEndpoint.startsWith("ws://")) {
throw new RuntimeException("Invalid web socket address: " + wsEndpoint);
}
return new BrowserServer(process, wsEndpoint);
} catch (IOException e) {
throw new PlaywrightException("Failed to launch server", e);
}
}

private BrowserServer(Process process, String wsEndpoint) {
this.process = process;
this.wsEndpoint = wsEndpoint;
}

void kill() throws InterruptedException {
process.destroy();
int exitCode = process.waitFor();
// FIXME: 2 tests are failing this check on windows:
// disconnectedEventShouldBeEmittedWhenBrowserIsClosedOrServerIsClosed
// shouldThrowWhenUsedAfterIsConnectedReturnsFalse
if (!isWindows) {
assertEquals(0, exitCode);
}
}

@Override
public void close() {
process.destroyForcibly();
}
}
28 changes: 28 additions & 0 deletions playwright/src/test/java/com/microsoft/playwright/HttpsServer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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;

/**
* The HTTPS test server, a distinct type so that it can be injected next to the HTTP {@link Server}.
*/
public class HttpsServer extends Server {
HttpsServer(int port) throws IOException {
super(port, true);
}
}
41 changes: 25 additions & 16 deletions playwright/src/test/java/com/microsoft/playwright/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,36 @@ private Auth(String user, String password) {
}
}

/** Listens on a port picked by the OS, which parallel tests cannot race for. */
public static Server createHttp() throws IOException {
return new Server(0, false);
}

public static Server createHttp(int port) throws IOException {
return new Server(port, false);
}

static Server createHttps(int port) throws IOException {
return new Server(port, true);
public static HttpsServer createHttps() throws IOException {
return new HttpsServer(0);
}

private Server(int port, boolean https) throws IOException {
PORT = port;
PREFIX = "http" + (https ? "s" : "") + "://localhost:" + PORT;
CROSS_PROCESS_PREFIX = "http" + (https ? "s" : "") + "://127.0.0.1:" + PORT;
EMPTY_PAGE = PREFIX + "/empty.html";
public static HttpsServer createHttps(int port) throws IOException {
return new HttpsServer(port);
}

protected Server(int port, boolean https) throws IOException {
if (https) {
HttpsServer httpsServer = HttpsServer.create(new InetSocketAddress("localhost", port), 0);
com.sun.net.httpserver.HttpsServer httpsServer = com.sun.net.httpserver.HttpsServer.create(new InetSocketAddress("localhost", port), 0);
httpsServer.setHttpsConfigurator(HttpsConfiguratorImpl.create());
server = httpsServer;
} else {
server = HttpServer.create(new InetSocketAddress("localhost", port), 0);
}
PORT = server.getAddress().getPort();
PREFIX = "http" + (https ? "s" : "") + "://localhost:" + PORT;
CROSS_PROCESS_PREFIX = "http" + (https ? "s" : "") + "://127.0.0.1:" + PORT;
EMPTY_PAGE = PREFIX + "/empty.html";

server.createContext("/", this);
server.setExecutor(null); // creates a default executor
server.start();
Expand All @@ -85,19 +94,19 @@ public void stop() {
server.stop(0);
}

void setAuth(String path, String user, String password) {
public void setAuth(String path, String user, String password) {
auths.put(path, new Auth(user, password));
}

void setCSP(String path, String csp) {
public void setCSP(String path, String csp) {
this.csp.put(path, csp);
}

void enableGzip(String path) {
public void enableGzip(String path) {
gzipRoutes.add(path);
}

void setResourceProvider(Function<String, InputStream> resourceProvider) {
public void setResourceProvider(Function<String, InputStream> resourceProvider) {
this.resourceProvider = resourceProvider;
}

Expand All @@ -118,7 +127,7 @@ static class Request {
}
}

Future<Request> futureRequest(String path) {
public Future<Request> futureRequest(String path) {
CompletableFuture<Request> future = requestSubscribers.get(path);
if (future == null) {
future = new CompletableFuture<>();
Expand All @@ -128,19 +137,19 @@ Future<Request> futureRequest(String path) {
}


void setRoute(String path, HttpHandler handler) {
public void setRoute(String path, HttpHandler handler) {
routes.put(path, handler);
}

void setRedirect(String from, String to) {
public void setRedirect(String from, String to) {
setRoute(from, exchange -> {
exchange.getResponseHeaders().put("location", Arrays.asList(to));
exchange.sendResponseHeaders(302, -1);
exchange.getResponseBody().close();
});
}

void reset() {
public void reset() {
requestSubscribers.clear();
auths.clear();
csp.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import java.security.cert.*;
import java.util.*;

public class ServerWithClientCertificate implements HttpHandler {
public class ServerWithClientCertificate implements HttpHandler, AutoCloseable {
private final HttpServer server;
final String origin;
final String crossOrigin;
Expand All @@ -41,7 +41,7 @@ private ServerWithClientCertificate(int port) throws IOException {
origin = "https://localhost:" + port;
crossOrigin = "https://127.0.0.1:" + port;;
url = origin + "/index.html";
HttpsServer httpsServer = HttpsServer.create(new InetSocketAddress("localhost", port), 0);
com.sun.net.httpserver.HttpsServer httpsServer = com.sun.net.httpserver.HttpsServer.create(new InetSocketAddress("localhost", port), 0);
httpsServer.setHttpsConfigurator(new HttpsConfiguratorImpl(loadCertificates()));
server = httpsServer;
server.createContext("/", this);
Expand All @@ -53,6 +53,11 @@ public void stop() {
server.stop(0);
}

@Override
public void close() {
stop();
}

private SSLContext loadCertificates() {
try {
// Create an SSL context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

package com.microsoft.playwright;

import com.microsoft.playwright.junit.FixtureTest;
import com.microsoft.playwright.junit.UsePlaywright;
import org.junit.jupiter.api.Test;
import com.microsoft.playwright.fixtures.PlaywrightTest;
import org.opentest4j.AssertionFailedError;

import java.io.OutputStreamWriter;
Expand All @@ -27,37 +25,35 @@
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@FixtureTest
@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class)
public class TestAPIResponseAssertions {
@Test
@PlaywrightTest
void passWithResponse(Page page, Server server) {
APIResponse res = page.request().get(server.EMPTY_PAGE);
assertThat(res).isOK();
}

@Test
@PlaywrightTest
void passWithNot(Page page, Server server) {
APIResponse res = page.request().get(server.PREFIX + "/unknown");
assertThat(res).not().isOK();
}

@Test
@PlaywrightTest
void fail(Page page, Server server) {
APIResponse res = page.request().get(server.PREFIX + "/unknown");
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> assertThat(res).isOK());
assertTrue(e.getMessage().contains("→ GET " + server.PREFIX + "/unknown"), "Actual error: " + e.toString());
assertTrue(e.getMessage().contains("← 404 Not Found"), "Actual error: " + e.toString());
}

@Test
@PlaywrightTest
void shouldPrintResponseTextIfIdOkFails(Page page, Server server) {
APIResponse res = page.request().get(server.PREFIX + "/unknown");
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> assertThat(res).isOK());
assertTrue(e.getMessage().contains("File not found"), "Actual error: " + e.toString());
}

@Test
@PlaywrightTest
void shouldOnlyPrintResponseWithTextContentTypeIfIsOkFails(Page page, Server server) {
{
server.setRoute("/text-content-type", exchange -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,15 @@
package com.microsoft.playwright;

import com.microsoft.playwright.assertions.LocatorAssertions;
import com.microsoft.playwright.junit.FixtureTest;
import com.microsoft.playwright.junit.UsePlaywright;
import org.junit.jupiter.api.Test;
import com.microsoft.playwright.fixtures.PlaywrightTest;
import org.opentest4j.AssertionFailedError;

import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

// Copied from expect-misc.spec.ts > toBeInViewport
@FixtureTest
@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class)
public class TestAssertThatIsInViewport {
@Test
@PlaywrightTest
void shouldWork(Page page) {
page.setContent("<div id=big style=\"height: 10000px;\"></div>\n" +
" <div id=small>foo</div>");
Expand All @@ -40,7 +36,7 @@ void shouldWork(Page page) {
assertThat(page.locator("#small")).isInViewport(new LocatorAssertions.IsInViewportOptions().setRatio(1));
}

@Test
@PlaywrightTest
void shouldRespectRatioOption(Page page) {
page.setContent("<style>body, div, html { padding: 0; margin: 0; }</style>\n" +
" <div id=big style=\"height: 400vh;\"></div>");
Expand All @@ -58,14 +54,14 @@ void shouldRespectRatioOption(Page page) {
assertThat(page.locator("div")).not().isInViewport(new LocatorAssertions.IsInViewportOptions().setRatio(0.8));
}

@Test
@PlaywrightTest
void shouldHaveGoodStack(Page page) {
AssertionFailedError error = assertThrows(AssertionFailedError.class, () -> assertThat(page.locator("body")).not().isInViewport(new LocatorAssertions.IsInViewportOptions().setTimeout(100)));
assertNotNull(error);
assertTrue(error.getMessage().contains("Locator expected not to be in viewport"), error.getMessage());
}

@Test
@PlaywrightTest
void shouldReportIntersectionEvenIfFullyCoveredByOtherElement(Page page) {
page.setContent("<h1>hello</h1>\n" +
" <div style=\"position: relative; height: 10000px; top: -5000px;></div>");
Expand Down
Loading
Loading