Home /
Modules / yoja-selenium
yoja-selenium
Browser-driven tests with a fluent TestBuilder: pick a
browser, start an embedded server, run named JS exports as test steps,
capture browser logs, integrate as JUnit 6 dynamic tests.
Installation
testImplementation 'com.easygoingapi:yoja-selenium:VERSION'
Requires a matching Selenium WebDriver on the test classpath
(chromedriver, geckodriver, msedgedriver),
either on PATH or via Selenium Manager.
Quick start
import com.easygoingapi.yoja.selenium.*;
import org.junit.jupiter.api.*;
import java.util.stream.Stream;
class TaskBoardTest {
@TestFactory
Stream<DynamicNode> scenarios() {
return TestBuilder.builder()
.browser(Browser.builder(Browser.CHROME)
.mode(Browser.Mode.HEADLESS)
.build())
.startYojaWeb()
.loadYwAssert()
.testJsUnit("/jsUnitSyncTest.js",
List.of("titleIsTaskManager", "loginFormIsPresent"))
.testAsyncModule("/jsUnitAsyncTest.js")
.stream();
}
}
Browser
Browser enum
Supported browsers. Each value maps to a Selenium WebDriver implementation.
Values
| Value | Description |
CHROME · FIREFOX · EDGE | Supported browsers; require the matching WebDriver on PATH or via Selenium Manager |
API
| Signature | Returns | Description |
getWebDriverClass() | Class<WebDriver> | Selenium driver class for this browser |
static builder(Browser) | Browser.Builder | Start a Config builder |
Browser.Mode
| Value | Description |
HEADLESS | No visible window |
HEADFUL | Visible window |
DEBUGGER | Pins all timeouts to 1 hour and opens devtools |
Browser Mode Debugger
Browser.Mode.DEBUGGER mode flips three switches at browser-launch time:
| Switch | Firefox | Chrome | Edge |
| DevTools opened on startup | -devtools arg | --auto-open-devtools-for-tabs | --auto-open-devtools-for-tabs |
| Window maximized | yes (after 1 s warm-up) | no (currently disabled) | yes (after 2 s warm-up) |
scriptTimeout
pageLoadTimeout
implicitlyWait | Duration.ofHours(1) | same | same |
The 1-hour timeouts matter most: without them, pausing at a Java breakpoint for more than the default 10 s would make Selenium abort the in-flight script and break the session. They give you room to inspect state at length without losing the browser context.
Browser.Mode.DEBUGGER mode does not pause on its own — it only prepares the environment. To actually pause, set a Java breakpoint in your IDE on the line of interest, or insert selenium.debugger() / builder.debugger() as a marker; the auto-opened DevTools then let you inspect the live DOM in parallel.
Which exception to break on. debugger() works by throwing — and immediately swallowing — a com.easygoingapi.yoja.selenium.Debugger exception (a RuntimeException). It never propagates, so it can never fail your test; its only purpose is to be a stable target for the IDE. To make debugger() pause, add an exception breakpoint on com.easygoingapi.yoja.selenium.Debugger (IntelliJ: Run ▸ View Breakpoints ▸ + ▸ Java Exception Breakpoint; Eclipse: Add Java Exception Breakpoint, keep "Caught" since the throw is caught inline) — every debugger() call then suspends the JVM at that throw while the browser stays alive thanks to the 1-hour timeouts.
Browser.Config (Builder)
| Signature | Returns | Description |
Builder.mode(Mode) | Builder | |
Builder.timeout(Duration) | Builder | Default wait timeout for element lookups |
Builder.build() | Browser.Config | |
getBrowser() | Browser | |
getMode() | Mode | |
getTimeout() | Duration | |
SeleniumService
SeleniumService class (AutoCloseable)
High-level wrapper around a WebDriver, tailored to driving Yoja-Web pages.
Factory
| Signature | Returns | Description |
static newInstance(Browser.Config) | SeleniumService | Launch the browser with the right options |
webDriver() | WebDriver | Underlying Selenium driver |
javascriptExecutor() | JavascriptExecutor | |
Navigation
| Signature | Returns | Description |
getHttpPage(HttpUrl | String) | void | Navigate the browser to the given URL |
getHttpPage(Duration, HttpUrl | String) | void | Navigate with an explicit wait timeout |
reload() | void | Reload the current page |
reload(ScriptOption) | void | Reload and re-inject helpers |
await(Duration) | void | Pause execution for the given duration |
resizeWindow(int w, int h) | void | Set the browser window dimensions |
Script execution
| Signature | Returns | Description |
executeScript(String | Path [, args]) | Object | Execute synchronous JS |
executeScript(Duration, String | Path, args) | Object | With an explicit timeout |
executeAsyncScript(String | Path [, args]) | Object | Execute async JS (must call the injected done callback) |
repeatScript(Duration until, String, args) | Object | Re-execute until a truthy value is returned or the timeout elapses |
repeatAsyncScript(Duration, String, args) | Object | Async variant of repeatScript |
DOM queries
| Signature | Returns | Description |
firstTag(css) | WebElement | First matching element (waits for presence) |
firstTag(Duration, css) | WebElement | With an explicit wait timeout |
firstTagFrom(WebElement, css) | WebElement | Scoped to a given element |
findTags(css) | List<WebElement> | All matching elements |
findTagsFrom(WebElement, css) | List<WebElement> | Scoped to a given element |
Storage accessors
| Signature | Returns | Description |
localStorage(key) | String | Raw value from window.localStorage |
localStorage(key, Storage) | String | Storage.date / type / value — extract a specific field from the stored JSON |
sessionStorage(key [, Storage]) | String | Same for window.sessionStorage |
Logs & helpers
| Signature | Returns | Description |
logs() | List<Log> | All browser-side log entries collected so far |
clearLogs() | void | Discard captured log entries |
saveLogs() | void | Persist logs to disk |
printLogs() | void | Print logs to stdout |
loadJavascript(String) | void | Inject a JS snippet into the current page |
loadYwAssert() | void | Inject the ywAssert.js assertion helper |
debugger() | void | IDE breakpoint hook |
close() | void | Quit the WebDriver |
TestBuilder
TestBuilder class
Fluent orchestrator. Builds a list of test steps, then runs them via execute() or exposes them as JUnit dynamic nodes via stream().
Factory
| Signature | Returns | Description |
static builder() | TestBuilder | |
Configuration
| Signature | Returns | Description |
browser(Browser.Config) | TestBuilder | Select browser and mode |
host(String) | TestBuilder | Override embedded server host |
contentType(String ext, String mime) | TestBuilder | Extension → MIME mapping for static serving |
webResource(String packageName [, contextPath [, url]]) | TestBuilder | Serve resources from a classpath package |
webResource(WebApp, String url) | TestBuilder | Serve resources from a WebApp |
webResource(WebResource) | TestBuilder | Serve a pre-built WebResource |
webService(WebService) | TestBuilder | Add a service endpoint to the embedded server |
webSocket(WebSocket) | TestBuilder | Add a WebSocket endpoint to the embedded server |
Bootstrap steps
| Signature | Returns | Description |
startJavascript([ScriptOption]) | TestBuilder | Step: serve a minimal HTML fixture and navigate to it |
startYojaWeb([ScriptOption]) | TestBuilder | Step: serve a full Yoja-Web fixture page and navigate to it. The page loads YojaWeb.js with yw-config-path="/YojaWeb.conf.js" — serve that path to configure yoja-web in tests |
getPage(String path) | TestBuilder | Step: navigate to the given path on the embedded server |
Test steps
| Signature | Returns | Description |
test(String name, Consumer<TestContext>) | TestBuilder | Add a custom named step |
testJsUnit(String path, List<String> functions, args...) | TestBuilder | Run named JS exports as individual test steps |
testModule(String path, args...) | TestBuilder | Run a synchronous JS module as a test step |
testAsyncModule([Duration,] String path, args...) | TestBuilder | Run an async JS module; optional timeout |
loadModule(String path) | TestBuilder | Step: load a JS module without asserting on its result |
repeatTestModuleUntil(Duration, String path, args...) | TestBuilder | Step: retry the module until it returns truthy or the timeout elapses |
Utility steps
| Signature | Returns | Description |
reload([ScriptOption]) | TestBuilder | Step: reload the current page |
resizeWindow(int w, int h) | TestBuilder | Step: resize the browser window |
await(Duration) | TestBuilder | Step: pause execution |
loadYwAssert() | TestBuilder | Step: inject ywAssert.js |
saveLogs() | TestBuilder | Step: persist browser logs to disk |
printLogs() | TestBuilder | Step: print browser logs to stdout |
debugger() | TestBuilder | Step: IDE breakpoint hook |
Run
| Signature | Returns | Description |
execute([boolean keepRunning]) | void | Run all steps sequentially; optionally keep the browser open after completion |
stream() | Stream<DynamicNode> | Expose steps as JUnit 6 @TestFactory dynamic nodes |
ScriptOption class
Controls which helpers are injected after page navigation.
| Signature | Returns | Description |
static apply() | ScriptOption | Fresh option with all flags off |
saveLogs() | ScriptOption | Install the ywLogger.js log capture helper |
loadYwAssert() | ScriptOption | Install the ywAssert.js assertion helper |
Test context
TestContext class (AutoCloseable)
Per-test handle injected into every Consumer<TestContext> step. Closing it shuts down both the embedded server and the WebDriver.
| Signature | Returns | Description |
browser() | Browser | Active browser enum value |
seleniumService() | SeleniumService | The wrapped WebDriver instance |
httpServerContext() | HttpServerContext | Embedded HTTP server details |
httpUrlBuilder() | HttpUrlBuilder | URL builder pre-configured for the embedded server |
logs() | List<Log> | Browser-side log entries |
getHttpPage(HttpUrl | String [, Duration]) | void | Navigate the browser; optional explicit timeout |
close() | void | Stop the embedded server and quit the WebDriver |
HttpServerContext · HttpUrlBuilder classes
Embedded Yoja HTTP server serving test fixtures. Port auto-assigned from a monotonically-increasing sequence starting at 8888.
HttpServerContext
| Signature | Returns | Description |
host() | String | Embedded server hostname |
port() | int | Auto-assigned port |
httpServerState() | State | Server lifecycle state |
httpUrlBuilder() | HttpUrlBuilder | URL builder pre-wired for this server |
close() | void | Stop the embedded server |
HttpUrlBuilder
| Signature | Returns | Description |
path(String | Path) | HttpUrlBuilder | |
query(String | HttpParameter) | HttpUrlBuilder | Query string |
fragment(String) | HttpUrlBuilder | URL hash fragment |
build() | HttpUrl | Build the full URL pointing at the embedded server |
ywAssert
ywAssert browser-side assertions
ywAssert.js injects a small JUnit-style assertion library into the page as the global window.ywAssert. Call it from JS test modules run via testModule / testAsyncModule / testJsUnit — every failed assertion throws a JavaScript Error that Selenium propagates back to Java and surfaces as a failing test step.
It is not injected by default. Make it available three equivalent ways:
// 1. as a script option when starting the page
builder.startYojaWeb(ScriptOption.apply().loadYwAssert());
// 2. as a dedicated TestBuilder step (before any test that needs it)
builder.loadYwAssert();
// 3. on demand from inside a test step
builder.test("...", ctx -> ctx.seleniumService().loadYwAssert());
API
| Method | Throws on failure |
fail(error) | Always throws Error(error) |
assertEquals(expected, actual, message?) | Throws unless JSON.stringify(expected) === JSON.stringify(actual) — deep equality for primitives, plain objects and arrays |
assertArrayEquals(expected, actual, message?) | Both must be arrays of equal length; each element compared via JSON.stringify; the error names the failing index and dumps both arrays |
assertTrue(value, message?) | Throws unless value === true (strict identity, not truthiness) |
assertFalse(value, message?) | Throws unless value === false (strict) |
assertNull(value, message?) | Throws unless value === null |
assertUndefined(value, message?) | Throws unless value === undefined |
assertNotNull(value, message?) | Throws if value === null |
assertNotUndefined(value, message?) | Throws if value === undefined |
The optional message is prepended to the error with " -> " for context.
Behavior notes
| Note | Detail |
| Strict equality only | assertTrue / assertFalse use ===, not truthiness — assertTrue(1) fails; only the literal true passes. Same for the null / undefined checks. |
JSON.stringify deep equality | Object key order matters ({a:1,b:2} ≠ {b:2,a:1}). Values that don't survive JSON (undefined, functions, Date, Map, Set, Symbol) are compared as their JSON form — convert them yourself first if precision matters. |
| Failure propagation | Each failure throws a plain Error. From an async module, catch it and forward via reject(error.message) so Selenium lifts it into a Java SeleniumException on the right step. |
Example — async test module
// /test/checkUserList.js
export default async function(args, resolve, reject) {
try {
const res = await fetch('/api/users')
const users = await res.json()
ywAssert.assertEquals(200, res.status, 'wrong HTTP status')
ywAssert.assertTrue(Array.isArray(users), 'response should be an array')
ywAssert.assertArrayEquals(
[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
users,
'user list payload')
resolve()
}
catch (e) {
reject(e.message)
}
}
Logs
Log · Log.Level classes
Browser-side log entry collected by ywLogger.js and surfaced via SeleniumService.logs().
Log accessors
| Signature | Returns | Description |
date() | Instant | Time the log entry was captured |
level() | Level | |
message() | String | |
Log.Level values
| Value | Description |
LOG · TRACE · DEBUG · INFO · WRAN · ERROR | Mirror the browser-side console levels; WRAN is intentional (matches the JS side) |
DateUtil · Debugger · SeleniumException helpers
| Class | Key API | Description |
DateUtil | formatTimestamp(Long | Instant) | Format a timestamp as yyyy-MM-dd HH:mm:ss in the JVM timezone |
Debugger | debugger() | IDE breakpoint hook — throws and immediately catches a Debugger exception |
SeleniumException | | Runtime exception for WebDriver, script and embedded-server failures |