moli-webdriver-smoke
This is a standalone WebDriver smoke project for moli. It is a peer of
moli-cdp-smoke/: the runner starts a local fixture server, starts
moli serve, and exercises the public WebDriver wire surface from outside
the Rust test harness.
The default suite runs every Moli group: raw WebDriver Classic HTTP, raw BiDi
WebSocket messages, Selenium Python's webdriver.Remote client, cross-engine
semantics and navigation contracts, Moli's URL policy, and renderer-IO script
interruption. This covers both protocol-level wire shape and real Selenium
client workflows without requiring a group allowlist.
The semantics group contains isolated cross-engine contracts. It can
run against Moli's native WebDriver endpoint or an external ChromeDriver;
Moli uses standard W3C capabilities without Chrome vendor options.
The url-policy group is a Moli hosted-product contract. It
uses isolated Classic and BiDi sessions to verify the exact local-file
navigation error envelopes, unchanged browsing contexts, and absence of BiDi
navigation lifecycle events. It is deliberately skipped for desktop Chromium,
whose browser-granted local-file capability is a different product policy.
The navigation-errors group is a cross-engine Chromium/WPT contract.
It runs the same 37-case public-wire matrix against Moli or ChromeDriver:
ten Classic malformed-body/URL cases, 22 BiDi invalid-argument cases, two
missing-context cases, and three real address failures. It also checks the W3C
HTTP and BiDi error envelopes, unchanged Classic URL, and post-failure session
liveness. The baseline was executed on 2026-08-09 with Debian Chromium
145.0.7632.116 and matching ChromeDriver 145.0.7632.117.
The script-timeout boundary has two deliberately separate groups:
script-timeout-chromiumis the ChromeDriver 147 oracle. A yielding async script times out near its deadline, while a bounded non-yielding loop first returns to the renderer and only then producesscript timeout.script-interruptis a default Moli group for the renderer-IO extension. It runs actual infinite loops through both Classic sync and async execution, requires timeout-drivenRuntime.terminateExecutionnear the configured deadline, and repeats recovery work in the same window after every termination.
The split is intentional: the Moli group proves preemption, while the ChromeDriver group records that ChromeDriver itself does not offer the same non-yielding WebDriver timeout behavior.
Coverage
- WebDriver Classic status/session/delete value envelopes.
- Classic navigation, current URL, title, page source, CSS/XPath element lookup and identity, element
text/tag/displayed/enabled/rect/attribute/property/computed label/computed
role, send keys, clear form controls with input/change events, file upload, click, execute script,
key actions whose handler performs a top-level Page replacement while the action request is completing,
explicit screenshot unsupported errors, cookies, alerts, unhandled prompt behavior, window-scoped prompt
switching, shadow roots, page-side SharedWorker probe without polluting window
handles,
document.open()replacement stale-element behavior, and headless window state surface. - WebDriver BiDi
session.status,session.new,browsingContext.create, exactly onebrowsingContext.userPromptOpened/userPromptClosednotification per alert/confirm/prompt underignore,accept, anddismiss, JavaScript return values after manual or automatic handling, and one-shot handling followed by live commands in the same session. Alert close events report acknowledgement for both automatic and manual dismissal. Popup dialogs work without acontextCreatedsubscription while their causing script is still pending. With lifecycle events subscribed, popup creation precedes prompt opening under manual handling and automatic acceptance/dismissal. Closing a popup while its prompt is waiting produces one prompt closure before one destruction. A nested popup with a configured preload channel checks parent/child creation order and modal progress. A shared Classic/BiDi session checks that explicit empty, omitted, and replacement BiDi text override text previously stored by Classic. The group also coverssession.subscribe,browsingContext.navigate, DOMContentLoaded lifecycle events,input.performActions,input.releaseActions, key actions that trigger a top-level Page replacement and remain successful through the replacement lifecycle, element-origin input action routing without asserting layout hit-test effects,input.setFiles,network.getData,browser.setDownloadBehavior,browsingContext.downloadWillBegin/downloadEnd,network.setCacheBehavior, SharedWorkerbrowsingContext.contextCreated, livescript.realmCreated(type = "shared-worker"), andscript.getRealms(type = "shared-worker"), user context creation/removal, user-context viewport inheritance, explicitbrowsingContext.captureScreenshotunsupported errors,emulation.setUserAgentOverride,emulation.setLocaleOverride,emulation.setTimezoneOverride,emulation.setNetworkConditions, storage isolation,storage.setCookie,storage.getCookies, andsession.end. Twenty-nine cross-enginebidi-networkscenarios cover iframe event ownership, request identity, subscription initialization, and navigation independence. Six hold Page A's HTTP response at an explicit server barrier while Page B resolves its exact realm and sends two channel messages. A then completes, closes, or accepts a replacement navigation before the held response is released; shared and isolated user contexts are covered. Closing must not revive A, and a late response must not replace its successor Document. Six keep the foreground tab active while an unattached background Page inserts, navigates, or nests an iframe. They cover shared and separate user contexts, require request/response and DOMContentLoaded/load events to identify the loaded child context, and read exact response bytes through the background Page's collector. The foreground collector must returnno such network data. Two more load distinct iframe responses in two live Pages, in shared or separate user contexts. Page-scoped collectors and a collector spanning both Pages must retain both bodies after the second request completes, and the request IDs must be distinct. Three subscribe globally or by the default user context as the first BiDi command on an existing Classic session, then insert an iframe without callinggetTreeor creating a Page. They require lifecycle events with and without Network events;getTreeruns only after those events arrive. Another subscribes on a Classic Page that already contains an iframe, checking parent-first context replay without duplicate notifications from overlapping subscriptions. Four Frame lifetime scenarios check stable IDs across iframe navigation, nested subtree removal, parent Document navigation, and an iframe created and removed synchronously in one script. Removed contexts must reject new subscriptions, destruction events must retain current URLs and nested children, and collected response bodies survive detach. Two more create a popup before subscribing, verifying its replayedoriginalOpeneragrees withgetTree, and navigate a middle iframe to a new Document. The middle Frame ID must survive, its old child must be destroyed exactly once and reject commands, and a later detach must not destroy the retired child again. Four popup cases check the initiating Frame'soriginalOpenerin live events, subscription replay andgetTree, including iframe openers and replay after the opener Page has closed. A final case completes twodocument.open/write/closecycles inside an iframe: each must emit one ordered DOMContentLoaded/load pair while preserving the initial navigation ID and Frame ID. All twenty-three identical scenarios passed against Moli and Chrome for Testing/ChromeDriver 156.0.8078.47 on 2026-10-29, with equal normalized semantic result records. The original six fail on the main revision preceding the owner fix. At the initial owner-fix revision, the separate-user-context collector returns the second response for the first request, and both first-command global subscriptions miss iframe lifecycle events. Before separating internal Frame facts from CDP Page subscriptions, the Frame lifetime cases miss context creation. The revision before preserving popup opener identity fails the replay scenario withoriginalOpener: null; middle-iframe navigation already passes there. Atd7f570cd49, all five latest scenarios fail: iframe popup attribution uses the Page ID, closed openers become null, and document-open lifecycle events are suppressed. The corrected implementation passes the same Chrome cases. Forty-threebidi-storagescenarios cover value, compound attribute, size, exact-path and expiry filters, non-ASCII string values, no matches, and empty/omitted filters that clear all cookies. Each scenario creates one unpartitioned cookie viastorage.setCookieand two CHIPS cookies via real HTTPSet-Cookieresponses (including a cross-site iframe), all sharing the same name/domain/path. It also seeds a different path and an unrelated name, then verifies every survivor and another user context remain unchanged. These scenarios run in the defaultbidi-storagegroup, with separate failure records under--continue-on-failure. Storage-key cases coversourceOriginnormalization and partition isolation,goog:hasCrossSiteAncestor, partitioned writes (including file sites), and malformed origins, invalid flags, missing user contexts and missing browsing contexts. Invalid commands must preserve every stored cookie. Writes target the supplied cookie domain even when the active document is on another site. Numeric cases enforce thejs-uintrange (0 through 2^53-1), reject invalid expiry/size values without changing any cookies, accept integer-valued JSON numbers such as21.0, and preserve valid unmatched filters at the upper bound. A maximum-safe expiry write remains persistent with Chrome's 400-day cap. Subdomain cases retain Chrome's distinction between the descriptor's normalized origin and the cookie's canonical site: writing withhttps://shop.example.comcreates a cookie athttps://example.com, while querying or deleting with the original subdomain matches nothing. This behavior is checked against Chrome rather than inferred from the storage-key helper's canonicalization rules. Context-owner cases pass real same-origin, cross-origin and nested iframe IDs frombrowsingContext.getTreeto get/set/delete. They verify unique child IDs and storage isolation across simultaneous user contexts, and reject removed frames and IDs retired by document replacement without changing cookies. The identical forty-three scenarios passed against Chrome for Testing and ChromeDriver 156.0.8078.47 on 2026-10-28. The returned partition key is checked for consistency betweengetCookiesanddeleteCookies; its supported fields can vary by engine under the WebDriver BiDi storage contract. - Selenium Python Remote WebDriver session creation, Selenium BiDi facade
webSocketUrlattachment withbrowsing_contextcreate/navigate/get_tree/ locate_nodes, browsing context fragment/history event handlers,scriptevaluate/console/javascript-error handlers, add/remove preload scripts with context filtering, channel arguments, sandbox realms, and userContext filtering,getRealms, root-owned script handles,callFunction,disown, special number/BigInt serialization, nested handle/local-value arguments,network.add_request_handler()request interception,network.add_auth_handler()Basic Auth continuation,browseruser context/client-window, andemulationuser-context UA/locale/timezone/viewport/network-conditions workflows, navigation, title/current URL, page source, element text/tag/attribute/property/css/displayed/enabled/ rect, link text locators, CSS/XPath user-facing locator workflows, accessible name, computed ARIA role, label-associated controls including link/img/select/textarea/input-button naming, send keys, clear, execute sync/async scripts with primitive/null/array/WebElement results, WebElement arguments, timeout/error recovery, and nested WebElement return values, Selenium client-side script pin/list/unpin workflows, Selenium text handling for child/block/inline content, whitespace, NBSP, hidden/script text exclusion, Selenium relative locators vialocate_with(), fetch/XHR through async scripts, localStorage, sessionStorage, IndexedDB, Selenium remote downloads via/se/files, frame switching by index/name/id/WebElement with nested parent/default-content recovery and relative lookup errors, deleted-frame recovery, SeleniumSelectsingle/multi-select option lookup, select/deselect, disabled/hidden option errors, form submit via SeleniumWebElement.submit()and submit-button click, checkbox/radio selection, text input/textarea typing and clear input/change events, all-printable key input, text-controlchangecommit on blur, WebDriver private-key caret navigation/PageUp/PageDown, arrow-key legacyKeyboardEvent.keyCode, selection/delete/backspace, numpad, and function-key send keys, direct stale element reference errors after navigation/DOM removal/document replacement, implicit waits for single and multiple element lookup, WebDriverWait expected conditions for presence/visibility/text/clickability/invisibility/staleness/ selection/value/attribute/frame/alert, W3C keyboard actions, and explicit unsupported-operation errors for pointer/wheel actions including hover/double-click/context-click/drag-drop, Selenium remote file upload via/se/filewith FileReader content reads, action key input with Shift modifiers, explicit screenshot/element screenshot and print_page unsupported errors, cookie management including SameSite, expired cookies, delete-all, and exact-name deletion, window handles/new/switch/close/rect/ maximize/minimize, anchor/script popup windows, named window target reuse,window.open(..., "_self"), SeleniumShadowRootlookup/scoped find/ execute_script equality/no-such-shadow-root errors, alert/confirm/prompt text, and alert accept/dismiss/send keys. - Cross-engine Selenium semantics for capability matching, same-document realm and WebElement identity, cross-document stale elements, top-level and popup storage ownership, nested frame recovery, shadow-frame Window named access, dialog return values, and standard W3C error classes. Each scenario uses an independent session and emits a structured contract record.
- Hosted local-file navigation rejection as Classic HTTP
500with W3Cunknown error, and as a BiDiunknown error; both retainabout:blank, and the BiDi path emits no immediate or delayed navigation lifecycle event. - Chromium/WPT navigation failure shapes: Classic HTTP
400invalid argumentenvelopes for null/missing/non-string/relative/malformed URLs; BiDiinvalid argumentfor context, URL, and wait validation;no such framefor missing context ids;unknown errorfor unsupported schemes, DNS failure, and unsafe ports; and a usable session after the complete failure matrix.
Known remaining worker-surface gap: the BiDi SharedWorker smoke now requires
target context creation, live realm creation, and script.getRealms; richer
lifecycle, error, and teardown coverage is still pending.
Usage
WebdriverIO v9 has a separate pinned client smoke, also run by CI. It checks capability opt-in and the same navigation, form input, click, and link workflow in Classic opt-out, default BiDi, and explicit BiDi sessions. BiDi sessions also check one shared node identity in both directions, pass Classic element and ShadowRoot references directly to BiDi before any BiDi lookup, and check stale-node errors after navigation for a reference never previously used by Classic:
npm ci --prefix moli-webdriver-smoke
MOLI_BIN="$PWD/target/debug/moli" npm run smoke:webdriverio --prefix moli-webdriver-smoke
Build moli first when no binary exists:
cargo build -p moli
Then run the default smoke:
cd moli-webdriver-smoke
uv sync
uv run moli-webdriver-smoke
With no --group, both local and CI runs execute all eight Moli groups:
classic,bidi,bidi-storage,selenium,semantics,url-policy,navigation-errors,script-interrupt
CI adds --continue-on-failure so one failing group does not hide later smoke
results. The ChromeDriver-only script-timeout-chromium oracle remains an
explicit external-target run.
List available groups:
uv run moli-webdriver-smoke --list-groups
Run focused groups:
uv run moli-webdriver-smoke --group classic
uv run moli-webdriver-smoke --group bidi
uv run moli-webdriver-smoke --group bidi-storage
uv run moli-webdriver-smoke --group selenium
uv run moli-webdriver-smoke --group semantics --continue-on-failure
uv run moli-webdriver-smoke --group url-policy --continue-on-failure
uv run moli-webdriver-smoke --group navigation-errors --continue-on-failure
uv run moli-webdriver-smoke --group script-interrupt
MOLI_WEBDRIVER_SMOKE_GROUPS=classic,bidi,selenium uv run moli-webdriver-smoke
Collect all subgroup failures in one diagnostic run:
uv run moli-webdriver-smoke \
--continue-on-failure
Run the same semantics group against a matching local Chrome/ChromeDriver
build:
~/chromium/src/out/Default/chromedriver --port=9515
uv run moli-webdriver-smoke \
--endpoint http://127.0.0.1:9515 \
--browser-name chrome \
--browser-binary ~/chromium/src/out/Default/chrome \
--group semantics --continue-on-failure
Run the same BiDi cookie deletion scenarios against ChromeDriver:
/path/to/chromedriver --port=9515
uv run moli-webdriver-smoke \
--endpoint http://127.0.0.1:9515 \
--browser-name chrome \
--browser-binary /path/to/chrome \
--group bidi-storage --continue-on-failure
Use matching Chrome and ChromeDriver versions. The fixture uses 127.0.0.1
and localhost as distinct top-level sites and waits for the iframe load
before checking the seeded cookies. No persistent browser profile is needed.
Validate additions or expectation changes against real Chrome with this same
group and record the browser/driver versions before adopting them as contracts.
Run the bounded ChromeDriver script-timeout oracle against the local Chromium 147 pair:
~/chromium/src/out/Default/chromedriver --port=9515
uv run moli-webdriver-smoke \
--endpoint http://127.0.0.1:9515 \
--browser-name chrome \
--browser-binary ~/chromium/src/out/Default/chrome \
--group script-timeout-chromium
Run Moli's non-yielding sync/async termination contract against moli serve:
uv run moli-webdriver-smoke --group script-interrupt
Run the Chromium/WPT navigation failure matrix against any matching local Chrome/ChromeDriver pair (the driver and browser major versions must agree):
/path/to/chromedriver-145.0.7632.117 --port=9516
uv run moli-webdriver-smoke \
--endpoint http://127.0.0.1:9516 \
--browser-name chrome \
--browser-binary /usr/bin/chromium \
--group navigation-errors
Useful environment variables:
MOLI_BIN: path to themolibinary under test.MOLI_WEBDRIVER_PORT: WebDriver server port. Defaults to a free local port.MOLI_WEBDRIVER_SMOKE_GROUPS: comma-separated smoke group list. CLI--grouptakes precedence.MOLI_WEBDRIVER_SMOKE_CONTINUE_ON_FAILURE=1: equivalent to--continue-on-failure.MOLI_WEBDRIVER_SMOKE_TRACE_BG=1: print backgroundmoli servelogs.
If both debug and release binaries exist, the runner uses the newest one by file
modification time. Set MOLI_BIN when you need an exact binary.
bidi-progress is part of the default suite. It pipelines real WebSocket
commands while an HTTP response, an unresolved Promise, an input pause, or
another page's prompt remains pending. It checks per-command terminal replies,
independent cancellation, top-level input-state sharing, and the performActions
queue. releaseActions bypasses the long performActions wait and only releases
already-dispatched input state. The Classic/attached-BiDi case records the
shared ChromeDriver boundary and requires settlement after the HTTP gate opens.
The navigation/prompt case opens the old Document's prompt only after the new navigation reaches a held HTTP response. Handling must return the real confirm result before that response is released, and the navigation must then finish. This avoids relying on Chrome's separate behavior of dismissing a prompt that was already open when navigation began.
The release/old/new input-generation case is calibrated against
Chrome/ChromeDriver 156.0.8078.73. Upstream mapper commit
49622edb
instead serializes perform/release on the same queue. The fixed-version oracle
does not establish the behavior of newer Chrome versions; this PR preserves
the tested input behavior.
Three additional groups require explicit selection:
bidi-input-layoutrequires a layout-enabled endpoint (for example,moli serve --layout --resource). It checks child-frame viewport and element origins with actual pointer hit testing; default Mock-mode error checks stay in the existingbidigroup.bidi-auth-progressdiagnoses concurrent authentication navigation and Native Fetch context ownership. It currently exposes a separate multi-target Fetch routing limitation in Moli and exits unsuccessfully when that occurs. It is deliberately not counted as a passing command-progress contract.bidi-layout-freshnessrequires a layout endpoint. With no input command in flight, it changes an element from left=20 to left=220, waits for a real animation-frame callback, and checks its geometry. The saved Moli baseline and this branch both expose stale geometry here; the diagnostic fails explicitly.
All progress cases keep their requests and received frames in the result JSON. No scenario retries failed commands or opens its HTTP gate to rescue a blocked control command. Passing the static pointer-origin tests does not establish dynamic geometry freshness; that separate rendering precondition must pass before interpreting a moving-element input test.