End-to-End Browser Testing with Quarkus and Playwright
A quick and practical guide to reliable, cross-browser testing for modern Java applications
I still see many Java teams treating browser tests as an afterthought. Either they are skipped entirely or replaced with brittle Selenium scripts that break every second sprint.
Modern web applications deserve better.
In this tutorial, we build a small Quarkus application and test it end-to-end using Playwright, integrated cleanly into the Quarkus testing model. No external test runners. No flaky waits. No magic.
By the end, you will have:
A real browser-tested Quarkus application
Stable, readable E2E tests
Cross-browser execution without code changes
A runtime Playwright use case
A CI pipeline that captures failures properly
This is how end-to-end testing should look in a modern Java stack.
Prerequisites
Make sure the following are installed locally:
JDK 21 or newer
Maven 3.9+
Quarkus CLI (optional, you can also use plain Maven)
All examples work with Quarkus 3.x.
Project Setup
We start with a minimal Quarkus application and add Playwright support.
Create the project
Start with below Quarkus CLI command or go directly to my Github repository and look at the code.
quarkus create app com.acme:tech-store --extension="rest,io.quarkiverse.playwright:quarkus-playwright"
cd tech-storeThis gives us a small REST-capable Quarkus application with static resource support and the Quarkus-Playwright extension which:
Manages Playwright lifecycle for tests
Downloads browser binaries when needed
Integrates with
@QuarkusTestHandles isolation and cleanup correctly
No custom test runner required.
Create the Target Application
End-to-end tests are useless without real behavior to verify. We keep the UI simple but realistic.
Create the HTML page
Create the file:
src/main/resources/META-INF/resources/index.html<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8”>
<title>Tech Store</title>
</head>
<body>
<h1>Welcome to the Tech Store</h1>
<div id=”search-section”>
<input type=”text” id=”product-search” placeholder=”Search products...”>
<button id=”search-btn”>Find</button>
</div>
<div id=”results”></div>
<script>
document.getElementById(’search-btn’).addEventListener(’click’, () => {
const query = document.getElementById(’product-search’).value;
const resultsDiv = document.getElementById(’results’);
if (query === ‘Laptop’) {
resultsDiv.innerHTML =
‘<div class=”item”>Gaming Laptop X1</div>’;
} else {
resultsDiv.innerHTML =
‘<p>No products found.</p>’;
}
});
</script>
</body>
</html>This page simulates:
A user typing a query
A button click
Client-side logic updating the DOM
Exactly the kind of interaction unit tests cannot validate.
Writing the End-to-End Test
Now we test the full user flow using a real browser. Remove the scaffolded tests before you create new ones.
Create the test class
src/test/java/com/acme/StoreTest.javapackage com.acme;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.AriaRole;
import io.quarkiverse.playwright.InjectPlaywright;
import io.quarkiverse.playwright.WithPlaywright;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.net.URL;
@QuarkusTest
@WithPlaywright
public class StoreTest {
@InjectPlaywright
Page page;
@TestHTTPResource(”/”)
URL appUrl;
@Test
void testProductSearch() {
page.navigate(appUrl.toString());
page.getByPlaceholder(”Search products...”)
.fill(”Laptop”);
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName(”Find”)).click();
Locator result = page.locator(”.item”);
Assertions.assertTrue(result.isVisible());
Assertions.assertEquals(
“Gaming Laptop X1”,
result.textContent());
}
}Why this works well
The Quarkus app is started automatically
A fresh browser page is injected per test
Playwright auto-waits for DOM changes
No sleeps. No polling. No race conditions
Run the test
./mvnw testBy default, this runs Chromium headless.
If you add
@WithPlaywright(debug=true)You can step through and debug the test.
Cross-Browser Testing via Configuration
Cross-browser testing should not duplicate test code. With Quarkus, it does not.
Configure browsers
You can change the target browsers by adding it to the
@WithPlaywright(browser = FIREFOX)The first run may download browser binaries.
Using Playwright at Runtime
Playwright is not limited to tests.
Sometimes you need a browser inside your application:
Generate screenshots
Render PDFs
Capture visual previews
Validate third-party pages
Create the service
Create: src/main/java/com/acme/ScreenshotService.java
package com.acme;
import com.microsoft.playwright.*;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.Base64;
@ApplicationScoped
public class ScreenshotService {
public String captureHomepageBase64(String url) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch(
new BrowserType.LaunchOptions()
.setHeadless(true)
);
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate(url);
byte[] screenshot = page.screenshot();
return Base64.getEncoder()
.encodeToString(screenshot);
}
}
}Important notes
Test-only annotations are not used here
Lifecycle is explicit
Headless mode is mandatory for servers
Try-with-resources guarantees cleanup
This pattern works in batch jobs, REST endpoints, or messaging consumers.
Create a small REST endpoint: src/main/java/com/acme/ScreenshotResource.java
package com.acme;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
@Path(”/screenshot”)
public class ScreenshotResource {
@Inject
ScreenshotService screenshotService;
@GET
@Produces(”image/png”)
public Response captureScreenshot(@QueryParam(”url”) String url) {
if (url == null || url.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(”URL parameter is required”)
.build();
}
try {
byte[] screenshot = screenshotService.captureHomepage(url);
return Response.ok(screenshot)
.header(”Content-Disposition”, “attachment; filename=\”screenshot.png\”“)
.type(”image/png”)
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(”Error capturing screenshot: “ + e.getMessage())
.build();
}
}
}And test it briefly with your application running:
curl "http://localhost:8080/screenshot?url=https://the-main-thread.com" -o screenshot.pngCI/CD with GitHub Actions
Browsers must exist in CI. Do not install them manually.
Create the workflow
.github/workflows/e2e-tests.ymlname: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.48.1-noble
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ‘21’
distribution: ‘temurin’
- name: Run tests
run: ./mvnw test Why this setup works
Official Playwright image
Correct browser versions
No flaky installs
What You Built
You now have:
A Quarkus application with a real UI
Stable end-to-end browser tests
Cross-browser execution via profiles
A runtime Playwright use case
CI diagnostics that actually help
End-to-end testing does not need to be slow, brittle, or painful.
With Quarkus and Playwright, it becomes:
Predictable
Observable
Maintainable
Test real behavior. Trust real browsers. Ship with confidence.





Thanks for this. It's just what I needed.
Note for Fedora/RHEL users: you will get a Playwright validation warning that looks worrying, but it can be ignored. Suppress it by adding the PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS environment variable to the maven-surefire-plugin configuration in pom.xml
This is exactly the kind of practical guide more Java shops need. The Playwright integration with Quarkus tests looks way smoother than what Ive seen with traditional Selenium setups. I really appreciate how you showed both the testing angle and teh runtime use case for screenshots - that second part often gets overlooked but its super valueable for production systems.