Skip to content

Commit fcbf305

Browse files
authored
test(fixtures): inject Playwright objects through JUnit 5 fixtures (#2017)
1 parent 911ebab commit fcbf305

176 files changed

Lines changed: 6569 additions & 4757 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CONTRIBUTING.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,53 @@ Each driver is assembled from the [`playwright-core`](https://www.npmjs.com/pack
4747

4848
```bash
4949
mvn compile
50+
# Runs every test in Chromium, Firefox and WebKit
5051
mvn test
52+
# Selecting browsers
53+
BROWSER=chromium mvn test
54+
BROWSER=firefox,webkit mvn test
5155
# Executing a single test
5256
BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize
5357
# Executing a single test class
5458
BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes
5559
```
5660

61+
Set `HEADED=1` to watch the browsers and `BROWSER_CHANNEL=chrome` (or `msedge`, ...) to test a stable channel.
62+
63+
### Writing tests
64+
65+
Tests use the fixtures from `playwright/src/test/java/com/microsoft/playwright/fixtures`, modeled after the
66+
upstream Playwright Test fixtures. A test is a `@PlaywrightTest` method that declares the fixtures it needs
67+
as parameters; it runs once per selected browser, in a fresh context:
68+
69+
```java
70+
@PlaywrightTest
71+
void shouldWork(Page page, Server server) {
72+
page.navigate(server.EMPTY_PAGE);
73+
}
74+
```
75+
76+
The built-in fixtures are `Playwright`, `BrowserName`, `BrowserType`, `BrowserType.LaunchOptions`, `Browser`,
77+
`Browser.NewContextOptions`, `BrowserContext`, `Page`, `Server` and `HttpsServer`. Playwright, the browser and
78+
the test servers are shared by the tests that run one after another on the same thread; the context and the
79+
page are created for each test and closed afterwards. A test class adds or overrides fixtures with
80+
`@Fixture` methods, whose parameters are the fixtures they depend on:
81+
82+
```java
83+
@Fixture
84+
Browser.NewContextOptions contextOptions() {
85+
return new Browser.NewContextOptions().setHasTouch(true);
86+
}
87+
88+
@Fixture(scope = Scope.WORKER)
89+
static BrowserType.LaunchOptions launchOptions(BrowserType.LaunchOptions defaults) {
90+
return defaults.setArgs(asList("--enable-features=WebMCP"));
91+
}
92+
```
93+
94+
Use `@DisabledOnBrowser` / `@EnabledOnBrowser` for browser-specific tests, and a `@Nested @ParameterizedClass`
95+
with a `@Fixture` method reading its `@Parameter` field to run the same tests with different options.
96+
5797
### Generating API
5898

5999
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:
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;
18+
19+
import com.microsoft.playwright.impl.driver.Driver;
20+
21+
import java.io.BufferedReader;
22+
import java.io.IOException;
23+
import java.io.InputStreamReader;
24+
import java.nio.file.Path;
25+
import java.util.Collections;
26+
27+
import static com.microsoft.playwright.fixtures.Env.isWindows;
28+
import static org.junit.jupiter.api.Assertions.assertEquals;
29+
30+
/**
31+
* A {@code playwright launch-server} process that tests connect to, the counterpart of the
32+
* upstream {@code startRemoteServer} fixture.
33+
*/
34+
class BrowserServer implements AutoCloseable {
35+
final Process process;
36+
final String wsEndpoint;
37+
38+
static BrowserServer launch(BrowserType browserType) {
39+
try {
40+
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
41+
Path dir = driver.driverDir();
42+
String node = dir.resolve(isWindows ? "node.exe" : "node").toString();
43+
String cliJs = dir.resolve("package/cli.js").toString();
44+
// We launch node process directly instead of using playwright.sh script as killing the script
45+
// process will leave node process running and killing it would be more hassle.
46+
ProcessBuilder pb = new ProcessBuilder(node, cliJs, "launch-server", "--browser", browserType.name());
47+
pb.directory(dir.toFile());
48+
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
49+
Process process = pb.start();
50+
BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()));
51+
String wsEndpoint = input.readLine();
52+
if (!wsEndpoint.startsWith("ws://")) {
53+
throw new RuntimeException("Invalid web socket address: " + wsEndpoint);
54+
}
55+
return new BrowserServer(process, wsEndpoint);
56+
} catch (IOException e) {
57+
throw new PlaywrightException("Failed to launch server", e);
58+
}
59+
}
60+
61+
private BrowserServer(Process process, String wsEndpoint) {
62+
this.process = process;
63+
this.wsEndpoint = wsEndpoint;
64+
}
65+
66+
void kill() throws InterruptedException {
67+
process.destroy();
68+
int exitCode = process.waitFor();
69+
// FIXME: 2 tests are failing this check on windows:
70+
// disconnectedEventShouldBeEmittedWhenBrowserIsClosedOrServerIsClosed
71+
// shouldThrowWhenUsedAfterIsConnectedReturnsFalse
72+
if (!isWindows) {
73+
assertEquals(0, exitCode);
74+
}
75+
}
76+
77+
@Override
78+
public void close() {
79+
process.destroyForcibly();
80+
}
81+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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;
18+
19+
import java.io.IOException;
20+
21+
/**
22+
* The HTTPS test server, a distinct type so that it can be injected next to the HTTP {@link Server}.
23+
*/
24+
public class HttpsServer extends Server {
25+
HttpsServer(int port) throws IOException {
26+
super(port, true);
27+
}
28+
}

‎playwright/src/test/java/com/microsoft/playwright/Server.java‎

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,27 +53,36 @@ private Auth(String user, String password) {
5353
}
5454
}
5555

56+
/** Listens on a port picked by the OS, which parallel tests cannot race for. */
57+
public static Server createHttp() throws IOException {
58+
return new Server(0, false);
59+
}
60+
5661
public static Server createHttp(int port) throws IOException {
5762
return new Server(port, false);
5863
}
5964

60-
static Server createHttps(int port) throws IOException {
61-
return new Server(port, true);
65+
public static HttpsServer createHttps() throws IOException {
66+
return new HttpsServer(0);
6267
}
6368

64-
private Server(int port, boolean https) throws IOException {
65-
PORT = port;
66-
PREFIX = "http" + (https ? "s" : "") + "://localhost:" + PORT;
67-
CROSS_PROCESS_PREFIX = "http" + (https ? "s" : "") + "://127.0.0.1:" + PORT;
68-
EMPTY_PAGE = PREFIX + "/empty.html";
69+
public static HttpsServer createHttps(int port) throws IOException {
70+
return new HttpsServer(port);
71+
}
6972

73+
protected Server(int port, boolean https) throws IOException {
7074
if (https) {
71-
HttpsServer httpsServer = HttpsServer.create(new InetSocketAddress("localhost", port), 0);
75+
com.sun.net.httpserver.HttpsServer httpsServer = com.sun.net.httpserver.HttpsServer.create(new InetSocketAddress("localhost", port), 0);
7276
httpsServer.setHttpsConfigurator(HttpsConfiguratorImpl.create());
7377
server = httpsServer;
7478
} else {
7579
server = HttpServer.create(new InetSocketAddress("localhost", port), 0);
7680
}
81+
PORT = server.getAddress().getPort();
82+
PREFIX = "http" + (https ? "s" : "") + "://localhost:" + PORT;
83+
CROSS_PROCESS_PREFIX = "http" + (https ? "s" : "") + "://127.0.0.1:" + PORT;
84+
EMPTY_PAGE = PREFIX + "/empty.html";
85+
7786
server.createContext("/", this);
7887
server.setExecutor(null); // creates a default executor
7988
server.start();
@@ -85,19 +94,19 @@ public void stop() {
8594
server.stop(0);
8695
}
8796

88-
void setAuth(String path, String user, String password) {
97+
public void setAuth(String path, String user, String password) {
8998
auths.put(path, new Auth(user, password));
9099
}
91100

92-
void setCSP(String path, String csp) {
101+
public void setCSP(String path, String csp) {
93102
this.csp.put(path, csp);
94103
}
95104

96-
void enableGzip(String path) {
105+
public void enableGzip(String path) {
97106
gzipRoutes.add(path);
98107
}
99108

100-
void setResourceProvider(Function<String, InputStream> resourceProvider) {
109+
public void setResourceProvider(Function<String, InputStream> resourceProvider) {
101110
this.resourceProvider = resourceProvider;
102111
}
103112

@@ -118,7 +127,7 @@ static class Request {
118127
}
119128
}
120129

121-
Future<Request> futureRequest(String path) {
130+
public Future<Request> futureRequest(String path) {
122131
CompletableFuture<Request> future = requestSubscribers.get(path);
123132
if (future == null) {
124133
future = new CompletableFuture<>();
@@ -128,19 +137,19 @@ Future<Request> futureRequest(String path) {
128137
}
129138

130139

131-
void setRoute(String path, HttpHandler handler) {
140+
public void setRoute(String path, HttpHandler handler) {
132141
routes.put(path, handler);
133142
}
134143

135-
void setRedirect(String from, String to) {
144+
public void setRedirect(String from, String to) {
136145
setRoute(from, exchange -> {
137146
exchange.getResponseHeaders().put("location", Arrays.asList(to));
138147
exchange.sendResponseHeaders(302, -1);
139148
exchange.getResponseBody().close();
140149
});
141150
}
142151

143-
void reset() {
152+
public void reset() {
144153
requestSubscribers.clear();
145154
auths.clear();
146155
csp.clear();

‎playwright/src/test/java/com/microsoft/playwright/ServerWithClientCertificate.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import java.security.cert.*;
2727
import java.util.*;
2828

29-
public class ServerWithClientCertificate implements HttpHandler {
29+
public class ServerWithClientCertificate implements HttpHandler, AutoCloseable {
3030
private final HttpServer server;
3131
final String origin;
3232
final String crossOrigin;
@@ -41,7 +41,7 @@ private ServerWithClientCertificate(int port) throws IOException {
4141
origin = "https://localhost:" + port;
4242
crossOrigin = "https://127.0.0.1:" + port;;
4343
url = origin + "/index.html";
44-
HttpsServer httpsServer = HttpsServer.create(new InetSocketAddress("localhost", port), 0);
44+
com.sun.net.httpserver.HttpsServer httpsServer = com.sun.net.httpserver.HttpsServer.create(new InetSocketAddress("localhost", port), 0);
4545
httpsServer.setHttpsConfigurator(new HttpsConfiguratorImpl(loadCertificates()));
4646
server = httpsServer;
4747
server.createContext("/", this);
@@ -53,6 +53,11 @@ public void stop() {
5353
server.stop(0);
5454
}
5555

56+
@Override
57+
public void close() {
58+
stop();
59+
}
60+
5661
private SSLContext loadCertificates() {
5762
try {
5863
// Create an SSL context

‎playwright/src/test/java/com/microsoft/playwright/TestAPIResponseAssertions.java‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616

1717
package com.microsoft.playwright;
1818

19-
import com.microsoft.playwright.junit.FixtureTest;
20-
import com.microsoft.playwright.junit.UsePlaywright;
21-
import org.junit.jupiter.api.Test;
19+
import com.microsoft.playwright.fixtures.PlaywrightTest;
2220
import org.opentest4j.AssertionFailedError;
2321

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

30-
@FixtureTest
31-
@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class)
3228
public class TestAPIResponseAssertions {
33-
@Test
29+
@PlaywrightTest
3430
void passWithResponse(Page page, Server server) {
3531
APIResponse res = page.request().get(server.EMPTY_PAGE);
3632
assertThat(res).isOK();
3733
}
3834

39-
@Test
35+
@PlaywrightTest
4036
void passWithNot(Page page, Server server) {
4137
APIResponse res = page.request().get(server.PREFIX + "/unknown");
4238
assertThat(res).not().isOK();
4339
}
4440

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

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

60-
@Test
56+
@PlaywrightTest
6157
void shouldOnlyPrintResponseWithTextContentTypeIfIsOkFails(Page page, Server server) {
6258
{
6359
server.setRoute("/text-content-type", exchange -> {

‎playwright/src/test/java/com/microsoft/playwright/TestAssertThatIsInViewport.java‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,15 @@
1717
package com.microsoft.playwright;
1818

1919
import com.microsoft.playwright.assertions.LocatorAssertions;
20-
import com.microsoft.playwright.junit.FixtureTest;
21-
import com.microsoft.playwright.junit.UsePlaywright;
22-
import org.junit.jupiter.api.Test;
20+
import com.microsoft.playwright.fixtures.PlaywrightTest;
2321
import org.opentest4j.AssertionFailedError;
2422

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

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

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

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

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

0 commit comments

Comments
 (0)