From 5a4ab608b68bc5c6d289d0faec61a0a209ce79a3 Mon Sep 17 00:00:00 2001 From: Stefan Ranoszek Date: Wed, 22 Apr 2026 11:27:35 +0100 Subject: [PATCH 1/4] feat: add Playwright browser manager alongside Selenium Add PlaywrightBrowser class with per-thread Playwright/Browser/Context/Page management. Activated via -Dautomation.engine=playwright Maven argument. - Thread-safe: each thread gets its own Playwright instance (no sharing) - Supports chromium, firefox, webkit, and Edge channel - Headless by default, configurable via -Dheadless=false - Added com.microsoft.playwright:playwright 1.52.0 dependency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pom.xml | 7 + .../driver/PlaywrightBrowser.java | 162 ++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 src/main/java/activesupport/driver/PlaywrightBrowser.java diff --git a/pom.xml b/pom.xml index 67b7698..0414790 100644 --- a/pom.xml +++ b/pom.xml @@ -580,6 +580,13 @@ ${webdrivermanager.version} + + + com.microsoft.playwright + playwright + 1.52.0 + + io.cucumber diff --git a/src/main/java/activesupport/driver/PlaywrightBrowser.java b/src/main/java/activesupport/driver/PlaywrightBrowser.java new file mode 100644 index 0000000..63ec3a5 --- /dev/null +++ b/src/main/java/activesupport/driver/PlaywrightBrowser.java @@ -0,0 +1,162 @@ +package activesupport.driver; + +import com.microsoft.playwright.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +/** + * Playwright browser manager, parallel to the Selenium {@link Browser} class. + *

+ * Each thread gets its own Playwright instance, Browser, BrowserContext, and Page + * (Playwright is NOT thread-safe, so nothing is shared across threads). + *

+ * Activate by passing {@code -Dautomation.engine=playwright} to Maven. + */ +public class PlaywrightBrowser { + + private static final Logger LOGGER = LogManager.getLogger(PlaywrightBrowser.class); + + private static final ThreadLocal threadPlaywright = new ThreadLocal<>(); + private static final ThreadLocal threadBrowser = new ThreadLocal<>(); + private static final ThreadLocal threadContext = new ThreadLocal<>(); + private static final ThreadLocal threadPage = new ThreadLocal<>(); + + public static boolean isPlaywright() { + return "playwright".equalsIgnoreCase(System.getProperty("automation.engine")); + } + + /** + * Returns the current thread's Playwright {@link Page}, creating one if needed. + * Analogous to {@link Browser#navigate()}. + */ + public static Page navigate() { + if (threadPage.get() == null) { + String browserName = System.getProperty("browser", "chrome"); + boolean headless = isHeadless(browserName); + launchBrowser(normaliseBrowserName(browserName), headless); + } + return threadPage.get(); + } + + public static Page getPage() { + return threadPage.get(); + } + + public static BrowserContext getContext() { + return threadContext.get(); + } + + public static boolean isBrowserOpen() { + return threadPage.get() != null; + } + + public static void closeBrowser() { + try { + Page page = threadPage.get(); + if (page != null) { + page.close(); + } + } catch (Exception e) { + LOGGER.warn("Error closing Playwright page: {}", e.getMessage()); + } finally { + threadPage.remove(); + } + + try { + BrowserContext ctx = threadContext.get(); + if (ctx != null) { + ctx.close(); + } + } catch (Exception e) { + LOGGER.warn("Error closing Playwright context: {}", e.getMessage()); + } finally { + threadContext.remove(); + } + + try { + com.microsoft.playwright.Browser browser = threadBrowser.get(); + if (browser != null) { + browser.close(); + } + } catch (Exception e) { + LOGGER.warn("Error closing Playwright browser: {}", e.getMessage()); + } finally { + threadBrowser.remove(); + } + + try { + Playwright pw = threadPlaywright.get(); + if (pw != null) { + pw.close(); + } + } catch (Exception e) { + LOGGER.warn("Error closing Playwright instance: {}", e.getMessage()); + } finally { + threadPlaywright.remove(); + } + } + + private static void launchBrowser(String browserType, boolean headless) { + LOGGER.info("Launching Playwright {} (headless={})", browserType, headless); + + Playwright pw = Playwright.create(); + threadPlaywright.set(pw); + + BrowserType type = switch (browserType) { + case "firefox" -> pw.firefox(); + case "webkit" -> pw.webkit(); + default -> pw.chromium(); + }; + + List defaultArgs = Arrays.asList( + "--no-sandbox", + "--disable-gpu", + "--disable-dev-shm-usage" + ); + + BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions() + .setHeadless(headless) + .setArgs(defaultArgs); + + // Edge channel support + if ("edge".equals(browserType)) { + launchOptions.setChannel("msedge"); + } + + com.microsoft.playwright.Browser browser = type.launch(launchOptions); + threadBrowser.set(browser); + + com.microsoft.playwright.Browser.NewContextOptions contextOptions = + new com.microsoft.playwright.Browser.NewContextOptions() + .setViewportSize(1920, 1080) + .setIgnoreHTTPSErrors(true); + + BrowserContext context = browser.newContext(contextOptions); + threadContext.set(context); + + Page page = context.newPage(); + threadPage.set(page); + } + + private static String normaliseBrowserName(String raw) { + if (raw == null) return "chrome"; + return switch (raw.toLowerCase().trim()) { + case "firefox", "firefox-proxy" -> "firefox"; + case "edge" -> "edge"; + case "webkit", "safari" -> "webkit"; + default -> "chrome"; // chrome, headless, chrome-proxy + }; + } + + private static boolean isHeadless(String raw) { + if (raw == null) return true; + String name = raw.toLowerCase().trim(); + // Default to headless unless explicitly running headed + String headlessProperty = System.getProperty("headless", "true"); + return "headless".equals(name) || "true".equalsIgnoreCase(headlessProperty); + } +} From bd41d3a16d64963ea4361918b7af8856c2c6effe Mon Sep 17 00:00:00 2001 From: Stefan Ranoszek Date: Mon, 27 Apr 2026 11:02:29 +0100 Subject: [PATCH 2/4] chore: playwright --- .../driver/PlaywrightBrowser.java | 176 ++++++------------ 1 file changed, 56 insertions(+), 120 deletions(-) diff --git a/src/main/java/activesupport/driver/PlaywrightBrowser.java b/src/main/java/activesupport/driver/PlaywrightBrowser.java index 63ec3a5..e8cec8a 100644 --- a/src/main/java/activesupport/driver/PlaywrightBrowser.java +++ b/src/main/java/activesupport/driver/PlaywrightBrowser.java @@ -4,159 +4,95 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.nio.file.Paths; import java.util.Arrays; import java.util.List; /** - * Playwright browser manager, parallel to the Selenium {@link Browser} class. + * Instance-based Playwright lifecycle manager. *

- * Each thread gets its own Playwright instance, Browser, BrowserContext, and Page - * (Playwright is NOT thread-safe, so nothing is shared across threads). + * Designed for composition — create one per test scenario (e.g. on a Cucumber World object) + * and call {@link #close()} in the teardown hook. No ThreadLocal or static state. *

- * Activate by passing {@code -Dautomation.engine=playwright} to Maven. + * Reads {@code -Dbrowser} and {@code -Dheadless} system properties. */ public class PlaywrightBrowser { private static final Logger LOGGER = LogManager.getLogger(PlaywrightBrowser.class); - private static final ThreadLocal threadPlaywright = new ThreadLocal<>(); - private static final ThreadLocal threadBrowser = new ThreadLocal<>(); - private static final ThreadLocal threadContext = new ThreadLocal<>(); - private static final ThreadLocal threadPage = new ThreadLocal<>(); - - public static boolean isPlaywright() { - return "playwright".equalsIgnoreCase(System.getProperty("automation.engine")); - } + private Playwright playwright; + private com.microsoft.playwright.Browser browser; + private BrowserContext context; + private Page page; /** - * Returns the current thread's Playwright {@link Page}, creating one if needed. - * Analogous to {@link Browser#navigate()}. + * Creates the Playwright browser, context and page. + * Call once at the start of a test scenario. */ - public static Page navigate() { - if (threadPage.get() == null) { - String browserName = System.getProperty("browser", "chrome"); - boolean headless = isHeadless(browserName); - launchBrowser(normaliseBrowserName(browserName), headless); - } - return threadPage.get(); - } + public void create() { + String browserName = System.getProperty("browser", "chrome"); + boolean headless = isHeadless(); + LOGGER.info("Creating Playwright session: browser={}, headless={}", browserName, headless); - public static Page getPage() { - return threadPage.get(); - } - - public static BrowserContext getContext() { - return threadContext.get(); - } + playwright = Playwright.create(); - public static boolean isBrowserOpen() { - return threadPage.get() != null; - } - - public static void closeBrowser() { - try { - Page page = threadPage.get(); - if (page != null) { - page.close(); - } - } catch (Exception e) { - LOGGER.warn("Error closing Playwright page: {}", e.getMessage()); - } finally { - threadPage.remove(); - } - - try { - BrowserContext ctx = threadContext.get(); - if (ctx != null) { - ctx.close(); - } - } catch (Exception e) { - LOGGER.warn("Error closing Playwright context: {}", e.getMessage()); - } finally { - threadContext.remove(); - } + BrowserType browserType = switch (normaliseBrowser(browserName)) { + case "firefox" -> playwright.firefox(); + case "webkit" -> playwright.webkit(); + default -> playwright.chromium(); + }; - try { - com.microsoft.playwright.Browser browser = threadBrowser.get(); - if (browser != null) { - browser.close(); - } - } catch (Exception e) { - LOGGER.warn("Error closing Playwright browser: {}", e.getMessage()); - } finally { - threadBrowser.remove(); + List args = Arrays.asList("--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"); + BrowserType.LaunchOptions opts = new BrowserType.LaunchOptions() + .setHeadless(headless) + .setArgs(args); + if ("edge".equalsIgnoreCase(browserName)) { + opts.setChannel("msedge"); } - try { - Playwright pw = threadPlaywright.get(); - if (pw != null) { - pw.close(); - } - } catch (Exception e) { - LOGGER.warn("Error closing Playwright instance: {}", e.getMessage()); - } finally { - threadPlaywright.remove(); - } + browser = browserType.launch(opts); + context = browser.newContext(new com.microsoft.playwright.Browser.NewContextOptions() + .setViewportSize(1920, 1080) + .setIgnoreHTTPSErrors(true)); + page = context.newPage(); } - private static void launchBrowser(String browserType, boolean headless) { - LOGGER.info("Launching Playwright {} (headless={})", browserType, headless); - - Playwright pw = Playwright.create(); - threadPlaywright.set(pw); - - BrowserType type = switch (browserType) { - case "firefox" -> pw.firefox(); - case "webkit" -> pw.webkit(); - default -> pw.chromium(); - }; - - List defaultArgs = Arrays.asList( - "--no-sandbox", - "--disable-gpu", - "--disable-dev-shm-usage" - ); - - BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions() - .setHeadless(headless) - .setArgs(defaultArgs); + /** Returns the current page, lazily creating the session if needed. */ + public Page page() { + if (page == null) create(); + return page; + } - // Edge channel support - if ("edge".equals(browserType)) { - launchOptions.setChannel("msedge"); - } + public BrowserContext context() { return context; } - com.microsoft.playwright.Browser browser = type.launch(launchOptions); - threadBrowser.set(browser); + public boolean isOpen() { return page != null; } - com.microsoft.playwright.Browser.NewContextOptions contextOptions = - new com.microsoft.playwright.Browser.NewContextOptions() - .setViewportSize(1920, 1080) - .setIgnoreHTTPSErrors(true); + /** Takes a full-page screenshot, or returns an empty byte array if no page is open. */ + public byte[] screenshot() { + return page != null ? page.screenshot() : new byte[0]; + } - BrowserContext context = browser.newContext(contextOptions); - threadContext.set(context); + /** + * Tears down page → context → browser → playwright in order. + * Safe to call multiple times. + */ + public void close() { + try { if (page != null) page.close(); } catch (Exception e) { LOGGER.warn("Error closing page: {}", e.getMessage()); } finally { page = null; } + try { if (context != null) context.close(); } catch (Exception e) { LOGGER.warn("Error closing context: {}", e.getMessage()); } finally { context = null; } + try { if (browser != null) browser.close(); } catch (Exception e) { LOGGER.warn("Error closing browser: {}", e.getMessage()); } finally { browser = null; } + try { if (playwright != null) playwright.close(); } catch (Exception e) { LOGGER.warn("Error closing playwright: {}", e.getMessage()); } finally { playwright = null; } + } - Page page = context.newPage(); - threadPage.set(page); + private boolean isHeadless() { + return "true".equalsIgnoreCase(System.getProperty("headless", "true")); } - private static String normaliseBrowserName(String raw) { + private String normaliseBrowser(String raw) { if (raw == null) return "chrome"; return switch (raw.toLowerCase().trim()) { case "firefox", "firefox-proxy" -> "firefox"; case "edge" -> "edge"; case "webkit", "safari" -> "webkit"; - default -> "chrome"; // chrome, headless, chrome-proxy + default -> "chrome"; }; } - - private static boolean isHeadless(String raw) { - if (raw == null) return true; - String name = raw.toLowerCase().trim(); - // Default to headless unless explicitly running headed - String headlessProperty = System.getProperty("headless", "true"); - return "headless".equals(name) || "true".equalsIgnoreCase(headlessProperty); - } } From 17321e2f9c5125c71614e4b4f500cd1abbb077cc Mon Sep 17 00:00:00 2001 From: Stefan Ranoszek Date: Tue, 28 Apr 2026 10:18:56 +0100 Subject: [PATCH 3/4] chore: playwright --- .../driver/PlaywrightBrowser.java | 95 +++++++++++-------- 1 file changed, 54 insertions(+), 41 deletions(-) diff --git a/src/main/java/activesupport/driver/PlaywrightBrowser.java b/src/main/java/activesupport/driver/PlaywrightBrowser.java index e8cec8a..3ca1bad 100644 --- a/src/main/java/activesupport/driver/PlaywrightBrowser.java +++ b/src/main/java/activesupport/driver/PlaywrightBrowser.java @@ -4,17 +4,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.Arrays; import java.util.List; -/** - * Instance-based Playwright lifecycle manager. - *

- * Designed for composition — create one per test scenario (e.g. on a Cucumber World object) - * and call {@link #close()} in the teardown hook. No ThreadLocal or static state. - *

- * Reads {@code -Dbrowser} and {@code -Dheadless} system properties. - */ public class PlaywrightBrowser { private static final Logger LOGGER = LogManager.getLogger(PlaywrightBrowser.class); @@ -24,66 +15,78 @@ public class PlaywrightBrowser { private BrowserContext context; private Page page; - /** - * Creates the Playwright browser, context and page. - * Call once at the start of a test scenario. - */ public void create() { String browserName = System.getProperty("browser", "chrome"); - boolean headless = isHeadless(); + boolean headless = "true".equalsIgnoreCase(System.getProperty("headless", "true")); LOGGER.info("Creating Playwright session: browser={}, headless={}", browserName, headless); playwright = Playwright.create(); - BrowserType browserType = switch (normaliseBrowser(browserName)) { - case "firefox" -> playwright.firefox(); - case "webkit" -> playwright.webkit(); - default -> playwright.chromium(); - }; + BrowserType browserType = resolveBrowserType(browserName); - List args = Arrays.asList("--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"); - BrowserType.LaunchOptions opts = new BrowserType.LaunchOptions() + BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions() .setHeadless(headless) - .setArgs(args); + .setArgs(launchArgs()); + if ("edge".equalsIgnoreCase(browserName)) { - opts.setChannel("msedge"); + launchOptions.setChannel("msedge"); } - browser = browserType.launch(opts); - context = browser.newContext(new com.microsoft.playwright.Browser.NewContextOptions() - .setViewportSize(1920, 1080) - .setIgnoreHTTPSErrors(true)); + configureLaunchOptions(launchOptions); + + browser = browserType.launch(launchOptions); + context = browser.newContext(contextOptions()); page = context.newPage(); } - /** Returns the current page, lazily creating the session if needed. */ public Page page() { if (page == null) create(); return page; } - public BrowserContext context() { return context; } + public BrowserContext context() { + return context; + } - public boolean isOpen() { return page != null; } + public boolean isOpen() { + return page != null; + } - /** Takes a full-page screenshot, or returns an empty byte array if no page is open. */ public byte[] screenshot() { return page != null ? page.screenshot() : new byte[0]; } - /** - * Tears down page → context → browser → playwright in order. - * Safe to call multiple times. - */ public void close() { - try { if (page != null) page.close(); } catch (Exception e) { LOGGER.warn("Error closing page: {}", e.getMessage()); } finally { page = null; } - try { if (context != null) context.close(); } catch (Exception e) { LOGGER.warn("Error closing context: {}", e.getMessage()); } finally { context = null; } - try { if (browser != null) browser.close(); } catch (Exception e) { LOGGER.warn("Error closing browser: {}", e.getMessage()); } finally { browser = null; } - try { if (playwright != null) playwright.close(); } catch (Exception e) { LOGGER.warn("Error closing playwright: {}", e.getMessage()); } finally { playwright = null; } + safeClose(page, "page"); + page = null; + safeClose(context, "context"); + context = null; + safeClose(browser, "browser"); + browser = null; + safeClose(playwright, "playwright"); + playwright = null; } - private boolean isHeadless() { - return "true".equalsIgnoreCase(System.getProperty("headless", "true")); + protected BrowserType resolveBrowserType(String browserName) { + return switch (normaliseBrowser(browserName)) { + case "firefox" -> playwright.firefox(); + case "webkit" -> playwright.webkit(); + default -> playwright.chromium(); + }; + } + + protected List launchArgs() { + return List.of("--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"); + } + + protected void configureLaunchOptions(BrowserType.LaunchOptions options) { + // Override to customise launch options + } + + protected com.microsoft.playwright.Browser.NewContextOptions contextOptions() { + return new com.microsoft.playwright.Browser.NewContextOptions() + .setViewportSize(1920, 1080) + .setIgnoreHTTPSErrors(true); } private String normaliseBrowser(String raw) { @@ -95,4 +98,14 @@ private String normaliseBrowser(String raw) { default -> "chrome"; }; } + + private void safeClose(AutoCloseable resource, String name) { + if (resource != null) { + try { + resource.close(); + } catch (Exception e) { + LOGGER.warn("Error closing {}: {}", name, e.getMessage()); + } + } + } } From 9955dbb18916de325290b183d1c7f09f21160094 Mon Sep 17 00:00:00 2001 From: Stefan Ranoszek Date: Tue, 28 Apr 2026 11:20:08 +0100 Subject: [PATCH 4/4] chore: playwright --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0414790..fd7dcb6 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ 3.2.5 + 1.52.0 4.36.0 5.4.0 5.11.3 @@ -584,7 +585,7 @@ com.microsoft.playwright playwright - 1.52.0 + ${playwright.version}