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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

- **Fix: `@PreCondition` session restore on a fresh browser** — the cached cookies and localStorage were silently dropped when the next test started on `about:blank`, so only the first test per thread was logged in. Restore now navigates to the origin the session was captured on first.

- **LambdaTest cloud provider** — `execution.mode: lambdatest` now creates W3C
`LT:Options` sessions, supports environment-based credentials and capability
overrides, and links each cloud session from the HTML report. (#197)

- **Cloud execution mode validation** — `execution.mode` now accepts the
existing `browserstack` and `saucelabs` providers, alongside `local`,
`remote`, and the new `lambdatest` provider.

- **Trace shows browser console errors** — when `browser.captureConsoleErrors: true`, the JS errors collected for a test now appear in its trace file under "Console Errors", next to the steps and failure details, so a shared trace includes what the browser logged. No change when the option is off. (#130)

- **Network mocking on Firefox via WebDriver BiDi** — `networkMock().stub(...)` now uses WebDriver BiDi network interception under the hood when supported by the driver (including Firefox and BiDi-enabled drivers), falling back to Chrome DevTools Protocol (CDP) for Chromium-based drivers. The existing stubbing API (`returnJson`, `returnBody`, `returnStatus`, `delay`, `clear`) works identically across Chrome and Firefox. (#196)
Expand Down
8 changes: 8 additions & 0 deletions docs-site/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ All notable changes to Selenium Boot are documented here.

## [Unreleased]

- **LambdaTest cloud provider** — `execution.mode: lambdatest` now creates W3C
`LT:Options` sessions, supports environment-based credentials and capability
overrides, and links each cloud session from the HTML report. (#197)

- **Cloud execution mode validation** — `execution.mode` now accepts the
existing `browserstack` and `saucelabs` providers, alongside `local`,
`remote`, and the new `lambdatest` provider.

- **Trace shows browser console errors** — when `browser.captureConsoleErrors: true`, the JS errors collected for a test now appear in its trace file under "Console Errors", next to the steps and failure details, so a shared trace includes what the browser logged. No change when the option is off. (#130)

- **Network mocking on Firefox via WebDriver BiDi** — `networkMock().stub(...)` now uses WebDriver BiDi network interception under the hood when supported by the driver (including Firefox and BiDi-enabled drivers), falling back to Chrome DevTools Protocol (CDP) for Chromium-based drivers. The existing stubbing API (`returnJson`, `returnBody`, `returnStatus`, `delay`, `clear`) works identically across Chrome and Firefox. (#196)
Expand Down
77 changes: 73 additions & 4 deletions docs-site/docs/cloud-execution.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
---
description: "Run Selenium tests on BrowserStack and Sauce Labs with zero code changes. Switch from local Chrome to cloud browsers by editing one config line."
description: "Run Selenium tests on BrowserStack, Sauce Labs, and LambdaTest with zero code changes. Switch from local Chrome to cloud browsers by editing one config line."
id: cloud-execution
title: Cloud Execution
sidebar_position: 12
---

# Cloud Execution

Selenium Boot supports running your test suite on **BrowserStack** and **Sauce Labs** with zero test-code changes. Switch from local Chrome to a cloud browser farm by changing one line in `selenium-boot.yml`.
Selenium Boot supports running your test suite on **BrowserStack**, **Sauce Labs**, and **LambdaTest** with zero test-code changes. Switch from local Chrome to a cloud browser farm by changing one line in `selenium-boot.yml`.

:::caution Requires Selenium Boot 3.2.1+
Cloud execution (`execution.mode: browserstack` or `saucelabs`) requires **Selenium Boot 3.2.1 or later**. It does not work in any earlier release — config loading rejects any `execution.mode` other than `local` or `remote` at startup — regardless of what the examples on this page show.
Cloud execution (`execution.mode: browserstack`, `saucelabs`, or `lambdatest`) requires **Selenium Boot 3.2.1 or later**.
:::

---
Expand All @@ -25,6 +25,7 @@ All four execution modes share the same driver lifecycle — tests use `getDrive
| `remote` | Your own Selenium Grid / Selenoid / Moon |
| `browserstack` | BrowserStack Automate cloud |
| `saucelabs` | Sauce Labs cloud |
| `lambdatest` | LambdaTest cloud |

---

Expand Down Expand Up @@ -242,6 +243,64 @@ Ensure your BrowserStack / Sauce Labs plan allows the number of concurrent sessi

---

## LambdaTest

### Prerequisites

1. Sign up at [lambdatest.com](https://www.lambdatest.com)
2. Copy your LambdaTest username and access key from your account settings

### Config

```yaml title="selenium-boot.yml"
execution:
mode: lambdatest
lambdatest:
username: ${LT_USERNAME}
accessKey: ${LT_ACCESS_KEY}
platform: "Windows 11"
browser: chrome
browserVersion: latest

browser:
name: chrome

timeouts:
explicit: 10
pageLoad: 30
```

The provider uses LambdaTest's W3C `LT:Options` capability and the
`https://hub.lambdatest.com/wd/hub` hub URL. Keys under `capabilities` are
merged into `LT:Options`, so they override the generated defaults:

```yaml
execution:
mode: lambdatest
lambdatest:
username: ${LT_USERNAME}
accessKey: ${LT_ACCESS_KEY}
platform: "Windows 11"
browser: chrome
browserVersion: latest
capabilities:
build: "v2.1.0"
video: true
```

Set environment variables before running:

```bash
export LT_USERNAME=your_username
export LT_ACCESS_KEY=your_access_key
mvn test
```

After each test, the HTML report includes a **☁ View Session** link to the
LambdaTest session logs.

---

## Switching between environments

Use Maven profiles or environment variables to switch execution targets without changing any YAML file:
Expand All @@ -265,7 +324,7 @@ Keep separate YAML files per environment — each imports shared settings and on

```yaml title="selenium-boot.yml"
execution:
mode: browserstack # local | remote | browserstack | saucelabs
mode: browserstack # local | remote | browserstack | saucelabs | lambdatest

# ── BrowserStack ───────────────────────────────────────────────
browserstack:
Expand All @@ -292,6 +351,16 @@ execution:
capabilities: # raw sauce:options overrides
recordVideo: true

# ── LambdaTest ─────────────────────────────────────────────────
lambdatest:
username: ${LT_USERNAME}
accessKey: ${LT_ACCESS_KEY}
platform: "Windows 11"
browser: chrome
browserVersion: latest
capabilities: # raw LT:Options overrides
video: true

browser:
name: chrome

Expand Down
2 changes: 1 addition & 1 deletion docs-site/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ browser:

# ── Execution ───────────────────────────────────────────────────────────────
execution:
mode: local # local | remote | browserstack | saucelabs
mode: local # local | remote | browserstack | saucelabs | lambdatest
baseUrl: https://your-app.com
gridUrl: http://localhost:4444 # only for remote mode

Expand Down
2 changes: 1 addition & 1 deletion docs-site/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Outcomes first — the API that delivers each one is on the right so you can jum
| **Verify what actually landed in the DB** | `db().assertRowExists()`, `db().query().assertValue()` — plain JDBC, no ORM |
| **Assert on the email your app sent** | `mailbox().waitForEmail(to("user@test.com"))` — Mailhog, Mailtrap, Outlook, IMAP |
| **Skip the browser for non-UI tests** | `@NoBrowser` — DB assertions, API checks, file operations, no WebDriver |
| **Run on real cloud browsers unchanged** | `execution.mode: browserstack` / `saucelabs` — session URL in HTML report |
| **Run on real cloud browsers unchanged** | `execution.mode: browserstack` / `saucelabs` / `lambdatest` — session URL in HTML report |
| **Understand failures without digging** | AI failure analysis — Claude explains why a test failed and suggests a fix |
| **Locators that repair themselves** | Self-healing fallback strategies when a locator fails |
| **Know which tests will flake next** | Flakiness prediction — risk scores from run history, radar chart in report |
Expand Down
2 changes: 1 addition & 1 deletion docs-site/docs/quarantine.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ A committed YAML file solves this cleanly:
| Survives fresh clones | ✅ — it's in git |
| No external services needed | ✅ — just a YAML file |
| Auditable | ✅ — changes go through PRs |
| Works with all execution modes | ✅ — local, remote, BrowserStack, Sauce Labs |
| Works with all execution modes | ✅ — local, remote, BrowserStack, Sauce Labs, LambdaTest |
| Works across team members | ✅ — everyone gets the same list |

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,14 @@ private static void validate(SeleniumBootConfig config) {
}

String mode = config.getExecution().getMode();
if (!"local".equalsIgnoreCase(mode) && !"remote".equalsIgnoreCase(mode)) {
if (!"local".equalsIgnoreCase(mode)
&& !"remote".equalsIgnoreCase(mode)
&& !"browserstack".equalsIgnoreCase(mode)
&& !"saucelabs".equalsIgnoreCase(mode)
&& !"lambdatest".equalsIgnoreCase(mode)) {
throw new IllegalStateException(
"execution.mode must be 'local' or 'remote', got: '" + mode + "'");
"execution.mode must be 'local', 'remote', 'browserstack', "
+ "'saucelabs', or 'lambdatest', got: '" + mode + "'");
}

if (config.getTimeouts() == null
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/seleniumboot/config/SeleniumBootConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ public void setMaxActiveSessions(int maxActiveSessions) {

private BrowserStack browserstack = new BrowserStack();
private SauceLabs saucelabs = new SauceLabs();
private LambdaTest lambdatest = new LambdaTest();

public LambdaTest getLambdatest() {return lambdatest;}
public void setLambdatest(LambdaTest v) {this.lambdatest = v;}

public BrowserStack getBrowserstack() { return browserstack; }
public void setBrowserstack(BrowserStack v) { this.browserstack = v; }
Expand Down Expand Up @@ -287,6 +291,28 @@ public static final class SauceLabs {
public java.util.Map<String, Object> getCapabilities() { return capabilities; }
public void setCapabilities(java.util.Map<String, Object> v) { this.capabilities = v != null ? v : new java.util.LinkedHashMap<>(); }
}

public static final class LambdaTest {
private String username;
private String accessKey;
private String platform = "Windows 11";
private String browser = "chrome";
private String browserVersion = "latest";
private java.util.Map<String, Object> capabilities = new java.util.LinkedHashMap<>();

public String getUsername() { return username; }
public void setUsername(String v) { this.username = v; }
public String getAccessKey() { return accessKey; }
public void setAccessKey(String v) { this.accessKey = v; }
public String getPlatform() { return platform; }
public void setPlatform(String v) { this.platform = v; }
public String getBrowser() { return browser; }
public void setBrowser(String v) { this.browser = v; }
public String getBrowserVersion() { return browserVersion; }
public void setBrowserVersion(String v){ this.browserVersion = v; }
public java.util.Map<String, Object> getCapabilities() { return capabilities; }
public void setCapabilities(java.util.Map<String, Object> v) { this.capabilities = v != null ? v : new java.util.LinkedHashMap<>(); }
}
}

public static final class Retry {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/seleniumboot/driver/DriverManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static void createDriver() {

DRIVER.set(driver);

// Capture cloud session URL for BrowserStack / Sauce Labs
// Capture cloud session URL for BrowserStack / Sauce Labs / LambdaTest
try {
String mode = SeleniumBootContext.getConfig().getExecution().getMode();
if (driver instanceof org.openqa.selenium.remote.RemoteWebDriver rdw) {
Expand All @@ -141,6 +141,8 @@ public static void createDriver() {
CLOUD_SESSION_URL.set(BrowserStackProvider.SESSION_URL_PREFIX + sessionId);
} else if ("saucelabs".equalsIgnoreCase(mode)) {
CLOUD_SESSION_URL.set(SauceLabsProvider.SESSION_URL_PREFIX + sessionId);
} else if ("lambdatest".equalsIgnoreCase(mode)) {
CLOUD_SESSION_URL.set(LambdaTestProvider.SESSION_URL_PREFIX + sessionId);
}
}
} catch (Exception ignored) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public static DriverProvider getProvider() {
return new SauceLabsProvider();
}

if ("lambdatest".equalsIgnoreCase(executionMode)) {
return new LambdaTestProvider();
}

// Custom providers registered via SPI or programmatically take precedence
DriverProvider custom = DriverProviderRegistry.find(browser);
if (custom != null) {
Expand Down
125 changes: 125 additions & 0 deletions src/main/java/com/seleniumboot/driver/LambdaTestProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.seleniumboot.driver;

import com.seleniumboot.config.SeleniumBootConfig;
import com.seleniumboot.internal.SeleniumBootContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.remote.AbstractDriverOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariOptions;

import java.net.URL;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Creates a RemoteWebDriver session against LambdaTest.
*/
public class LambdaTestProvider implements DriverProvider {

public static final String HUB_URL
= "https://hub.lambdatest.com/wd/hub";

public static final String SESSION_URL_PREFIX
= "https://automation.lambdatest.com/logs/?sessionID=";

@Override
public WebDriver createDriver() {
SeleniumBootConfig cfg = SeleniumBootContext.getConfig();
SeleniumBootConfig.Execution.LambdaTest lt
= cfg.getExecution().getLambdatest();

String username = BrowserStackProvider.resolveEnv(lt.getUsername());
String accessKey = BrowserStackProvider.resolveEnv(lt.getAccessKey());
String browser = lt.getBrowser() != null
? lt.getBrowser() : "chrome";
String testId = SeleniumBootContext.getCurrentTestId();

if (isBlank(username)) {
throw new IllegalStateException(
"[LambdaTest] execution.lambdatest.username is required");
}

if (isBlank(accessKey)) {
throw new IllegalStateException(
"[LambdaTest] execution.lambdatest.accessKey is required");
}

Map<String, Object> ltOptions = new LinkedHashMap<>();
ltOptions.put("username", username);
ltOptions.put("accessKey", accessKey);

if (!isBlank(testId)) {
ltOptions.put("name", testId);
}

// User-provided LT:Options values override defaults.
if (lt.getCapabilities() != null) {
ltOptions.putAll(lt.getCapabilities());
}

AbstractDriverOptions<?> options = buildOptions(
browser,
lt.getBrowserVersion(),
lt.getPlatform(),
ltOptions
);

try {
WebDriver driver
= new RemoteWebDriver(new URL(HUB_URL), options);

driver.manage().timeouts().implicitlyWait(Duration.ZERO);
driver.manage().timeouts().pageLoadTimeout(
Duration.ofSeconds(cfg.getTimeouts().getPageLoad()));

return driver;
} catch (Exception e) {
throw new IllegalStateException(
"[LambdaTest] Failed to create session: "
+ e.getMessage(), e);
}
}

private static AbstractDriverOptions<?> buildOptions(
String browser,
String browserVersion,
String platform,
Map<String, Object> ltOptions) {

AbstractDriverOptions<?> options;

switch (browser.toLowerCase()) {
case "firefox":
options = new FirefoxOptions();
break;
case "edge":
options = new EdgeOptions();
break;
case "safari":
options = new SafariOptions();
break;
default:
options = new ChromeOptions();
break;
}

if (!isBlank(browserVersion)) {
options.setBrowserVersion(browserVersion);
}

if (!isBlank(platform)) {
options.setPlatformName(platform);
}

options.setCapability("LT:Options", ltOptions);
return options;
}

private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
}
Loading
Loading