A V language implementation of the W3C WebDriver protocol for browser automation - Fast, safe, and Selenium-compatible
20
stars
117
commits
V
primary language
Jul 22, 2026
updated
A V language implementation of the W3C WebDriver protocol for browser automation.
Version 5.1.0 | Playwright-style API + WebDriver-BiDi + native iOS/Android + codegen | 100% Selenium parity | Production Ready
Beyond the raw W3C WebDriver methods, Vebidor ships a higher-level ergonomic layer: one-call browser launch, lazy auto-waiting Locators, semantic selector engines, and retrying web-first assertions.
import vebidor.webdriver
fn main() {
// Auto-detects the driver + browser, picks a free port, and tears
// everything down on close(). No manual "start chromedriver" step.
mut b := webdriver.launch_edge(webdriver.LaunchOptions{ headless: true })!
defer { b.close() }
b.goto('https://example.com')!
// Locators are lazy and auto-wait for the element to be actionable.
b.wd.get_by_role('link', 'More information...').click()!
// Web-first assertions poll until the condition holds (or time out).
webdriver.expect(b.wd.get_by_role('heading', '')).to_be_visible()!
}
Selector engines: get_by_role, get_by_text, get_by_label,
get_by_placeholder, get_by_test_id, plus wd.locator('css=...' | 'xpath=...')
with chaining (row.locator('td')) and nth(i) / first().
Auto-waiting actions: click, fill, type_text, clear wait for the
element to be attached, visible, enabled, and scrolled into view.
Assertions: to_be_visible / to_be_hidden / to_be_enabled /
to_be_disabled, to_have_text / to_contain_text / to_have_value /
to_have_attribute / to_have_count, each invertible via .not() and tunable
via .with_timeout(ms).
Launch with bidi: true to get a persistent WebSocket alongside the Classic
session, unlocking event-driven features Classic cannot offer — network
interception/mocking, console/network listeners, isolated contexts, and more.
mut b := webdriver.launch_edge(webdriver.LaunchOptions{ headless: true, bidi: true })!
defer { b.close() }
mut bidi := b.bidi()!
defer { bidi.close() }
ctx := bidi.first_context()!
// Mock a response (Playwright route.fulfill style).
bidi.route(fn (req webdriver.InterceptedRequest) {
if req.url.contains('/api') {
req.fulfill(200, 'application/json', '{"mocked":true}') or {}
} else {
req.continue_request() or {}
}
})!
bidi.add_preload_script('() => { window.__patched = true }')! // runs before page scripts
bidi.navigate(ctx, 'https://example.com')!
bidi.on_log(fn (e webdriver.LogEntry) { println('${e.level}: ${e.text}') })!
BiDi capabilities: request/response interception + mocking (route,
route_response, fulfill/abort/continue), HTTP auth (on_auth),
console/network events (on_log/on_request/on_response), wait_for_event,
isolated user contexts (with per-context proxy, geolocation,
permissions, and storageState session reuse), preload scripts +
call_function, viewport emulation, partition-aware cookies
(get_cookies(user_context: uc)) + on_cookie_changed, screenshots/PDF, file
upload (set_files), and a Tracer. Capability probing via status() /
supports(). Any unwrapped command/event is reachable via send/on (or
on_sync for inline observers).
Mobile emulation (Playwright-style): device presets
(emulate_device(ctx, 'iPhone 14'), 9 built-in), emulate() for viewport/DPR/UA/
touch flags, Locator.tap(), set_request_user_agent (server-side UA), and
set_locale/set_timezone/set_screen_orientation. (Real touch-event dispatch
needs a CDP/mobileEmulation capability BiDi doesn't expose; touch detection is
emulated.)
See COMPARISON_WITH_PLAYWRIGHT.md for the full Playwright/Selenium feature mapping.
Core Features:
launch(), lazy auto-waiting Locators, selector engines, and web-first assertions (see ⚡ Modern API)Phase 1 & 5 - Element Properties ✨ ✅ 100% Complete:
is_displayed), enabled state (is_enabled), selection state (is_selected)get_css_value) - colors, fonts, dimensions, spacingPhase 2 - Alert Handling ✨:
Phase 3 - Page Information ✨:
Phase 4 - Window & Waits ✨:
Phase 6 - Expected Conditions ✨:
Phase 7 - Advanced Actions ✨:
Phase 8 - Async JS & Shadow DOM ✨ ✅ 100% Complete:
Edge-Specific:
The fastest way to get vebidor onto V's module path:
v install vebidor
This pulls the module so import vebidor.webdriver and import vebidor.mobile
resolve from anywhere. To track main instead of a published release, clone and
symlink the checkout:
git clone https://github.com/quaesitor-scientiam/vebidor.git
cd vebidor
mkdir -p ~/.vmodules
ln -s "$(pwd)" ~/.vmodules/vebidor
# (Alternatively: `v install` from this directory.)
Tip: With the Modern API
launch(), you can skip the manual driver-start step below — Vebidor finds the driver onPATH(or viaEDGEDRIVER/CHROMEDRIVER/GECKODRIVER), starts it on a free port, and stops it for you. The steps below are for the classicnew_*_driver(url, caps)flow that connects to a driver you started yourself.
# Start your browser's WebDriver
# Edge/Chrome:
.\msedgedriver.exe --port=9515 # or chromedriver.exe
# Firefox:
.\geckodriver.exe --port=4444
# Safari (macOS):
safaridriver --enable
safaridriver -p 4445
Need help? See TEST_ENVIRONMENT_SETUP.md for detailed setup instructions.
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'msedge'
edge_options: webdriver.EdgeOptions{
args: ['--headless=new']
binary: r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
}
}
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
title := wd.get_title()!
println('Page title: ${title}')
}
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'chrome'
chrome_options: webdriver.ChromeOptions{
args: ['--headless=new', '--disable-gpu']
binary: r'C:\Program Files\Google\Chrome\Application\chrome.exe'
}
}
wd := webdriver.new_chrome_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
println('Chrome automation working!')
}
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'firefox'
firefox_options: webdriver.FirefoxOptions{
args: ['-headless']
binary: r'C:\Program Files\Mozilla Firefox\firefox.exe'
}
}
wd := webdriver.new_firefox_driver('http://127.0.0.1:4444', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
println('Firefox automation working!')
}
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'safari'
safari_options: webdriver.SafariOptions{
automatic_inspection: false
automatic_profiling: false
}
}
wd := webdriver.new_safari_driver('http://127.0.0.1:4445', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
println('Safari automation working!')
}
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'msedge'
edge_options: webdriver.EdgeOptions{
args: ['--headless=new']
}
}
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
// Set timeouts (Phase 4)
wd.set_implicit_wait(10000)!
wd.set_page_load_timeout(30000)!
// Navigate and get page info (Phase 3)
wd.get('https://example.com')!
title := wd.get_title()!
url := wd.get_current_url()!
println('Page: ${title} at ${url}')
// Element properties (Phase 1)
heading := wd.find_element('css selector', 'h1')!
text := wd.get_text(heading)!
tag := wd.get_tag_name(heading)!
visible := wd.is_displayed(heading)!
println('Found <${tag}>: "${text}" (visible: ${visible})')
// Handle alerts (Phase 2)
wd.execute_script('alert("Test")', [])!
alert_text := wd.get_alert_text()!
println('Alert says: ${alert_text}')
wd.accept_alert()!
// Multi-window (Phase 4)
new_tab := wd.new_window('tab')!
wd.switch_to_window(new_tab.handle)!
wd.get('https://www.iana.org')!
wd.maximize_window()!
}
import vebidor.webdriver
fn login_example() ! {
caps := webdriver.Capabilities{
browser_name: 'msedge'
}
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com/login')!
// Find form elements
username := wd.find_element('css selector', '#username')!
password := wd.find_element('css selector', '#password')!
submit := wd.find_element('css selector', 'button[type="submit"]')!
// Fill the form
wd.send_keys(username, 'testuser')!
wd.send_keys(password, 'password123')!
wd.click(submit)!
}
import vebidor.webdriver
fn cookie_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Add a cookie
cookie := webdriver.Cookie{
name: 'session_id'
value: 'abc123'
path: '/'
domain: 'example.com'
}
wd.add_cookie(cookie)!
// Get all cookies
cookies := wd.get_cookies()!
println('Cookies: ${cookies.len}')
// Delete a cookie
wd.delete_cookie('session_id')!
}
import vebidor.webdriver
fn actions_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Type text using actions
wd.type_text('Hello World')!
// Scroll down
wd.scroll_by(0, 500)!
// Click at coordinates
wd.click_at(100, 200)!
}
import vebidor.webdriver
fn element_properties_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
link := wd.find_element('css selector', 'a')!
// Get element properties
text := wd.get_text(link)! // Visible text
href := wd.get_attribute(link, 'href')! // HTML attribute
tag := wd.get_tag_name(link)! // Tag name
// Check element state
visible := wd.is_displayed(link)!
enabled := wd.is_enabled(link)!
println('Link: ${text} -> ${href}')
println('Visible: ${visible}, Enabled: ${enabled}')
}
import vebidor.webdriver
fn alert_handling_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Handle alert
wd.execute_script('alert("Hello!")', [])!
text := wd.get_alert_text()!
println('Alert: ${text}')
wd.accept_alert()!
// Handle prompt
wd.execute_script('window.name = prompt("Your name?")', [])!
wd.send_alert_text('Claude')!
wd.accept_alert()!
}
import vebidor.webdriver
fn page_info_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Get page metadata
title := wd.get_title()!
url := wd.get_current_url()!
source := wd.get_page_source()!
println('Title: ${title}')
println('URL: ${url}')
println('HTML length: ${source.len} bytes')
// Verify navigation
assert title == 'Example Domain'
assert url == 'https://example.com/'
}
import vebidor.webdriver
fn multi_window_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Get current window
main_window := wd.get_window_handle()!
// Create new tab
new_tab := wd.new_window('tab')!
wd.switch_to_window(new_tab.handle)!
// Navigate in new tab
wd.get('https://www.iana.org')!
new_title := wd.get_title()!
// Switch back to main window
wd.switch_to_window(main_window)!
// Window state management
wd.maximize_window()!
wd.fullscreen_window()!
}
import vebidor.webdriver
fn timeouts_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
// Configure timeouts
wd.set_implicit_wait(10000)! // Auto-wait 10s for elements
wd.set_page_load_timeout(30000)! // 30s page load timeout
wd.set_script_timeout(15000)! // 15s script timeout
wd.get('https://example.com')!
// Now element finding automatically waits
element := wd.find_element('css selector', 'h1')!
// Custom wait condition
wd.wait_for(fn (wd webdriver.WebDriver) !bool {
title := wd.get_title()!
return title.len > 0
}, 5000, 500)!
}
v test webdriver/quick_test.v
v test webdriver/
v run integration_test.v
v simple_test.v
.\simple_test.exe
examples/webdriver_latency_bench.v measures round-trip latency for common WebDriver commands (get_title, get_current_url, execute_script) and reports min/avg/p50/p95/p99/max in milliseconds.
Prerequisites: a WebDriver must already be running before launching the benchmark.
# Default (stdlib net.http)
v run examples/webdriver_latency_bench.v
# Raw TCP transport (opt-in, avoids keep-alive stall)
v -d wd_use_raw_tcp run examples/webdriver_latency_bench.v
Options:
| Flag | Default | Description |
|---|---|---|
--browser=edge|chrome|firefox|safari | edge | Browser to use |
--driver=URL | http://127.0.0.1:9515 | WebDriver URL |
--iters=N | 200 | Number of timed iterations |
--warmup=N | 20 | Warmup iterations (not measured) |
--binary=PATH | (env var) | Path to browser executable |
--headless / --headed | headless | Run browser headlessly or not |
Example output:
Command: get_title
count: 200 min: 2.007 ms avg: 2.460 ms p50: 2.344 ms p95: 3.045 ms p99: 4.023 ms max: 4.545 ms
Beyond browser automation, Vebidor ships a native mobile module
(vebidor.mobile) that drives real apps on iOS and Android — by
talking directly to WebDriverAgent (iOS) and the UiAutomator2 server
(Android), the same on-device backends Appium uses, but with no Node
middleware.
import vebidor.mobile
mut s := mobile.launch_android(mobile.AndroidOptions{
mode: .spawn
udid: 'emulator-5554'
app_package: 'com.android.settings'
app_activity: '.Settings'
uia2_server_apk: server_apk
uia2_server_test_apk: test_apk
})!
defer { s.close() }
// Same Playwright-style surface as the web module:
mobile.expect(s.get_by_text('Battery')).to_be_visible()!
s.get_by_label('Battery').tap()!
s.set_orientation(.landscape)!
One API, two backends. Cross-platform get_by_label/text/test_id/role
selectors, lazy auto-waiting locators, expect() assertions, touch
gestures, app lifecycle, and device state (orientation, lock, geolocation,
network) — all sharing the web module's transport and polling engine. The
Android path is verified live on an emulator; iOS on a Simulator.
See COMPARISON_WITH_APPIUM.md for the Appium feature mapping and MOBILE_TESTING.md for setup.
Current: 🎉 100% feature parity with Selenium WebDriver! 🎉 ALL Phases Complete! 🎊
| Category | Status |
|---|---|
| Session Management | ✅ 100% |
| Navigation | ✅ 100% |
| Element Location | ✅ 100% |
| Cookies | ✅ 100% |
| Screenshots | ✅ 100% |
| Frames | ✅ 100% |
| Element Properties | ✅ 100% (Phase 1, 5 & 7) |
| Alerts | ✅ 100% (Phase 2) |
| Page Information | ✅ 100% (Phase 3) |
| Window Management | ✅ 100% (Phase 4) |
| Timeouts & Waits | ✅ 100% (Phase 4 & 6) |
| Actions API | ✅ 100% (Phase 7) |
| Element Interaction | ✅ 100% (Phase 7) |
| JavaScript Execution | ✅ 100% (Phase 8) ✨ NEW |
| Shadow DOM | ✅ 100% (Phase 8) ✨ NEW |
🏆 100% FEATURE PARITY ACHIEVED! 🏆
See COMPARISON_WITH_PLAYWRIGHT.md for the full Playwright/Selenium feature mapping.
🎬 Codegen / session recorder — record a live session and emit runnable
vebidor source via tools/codegen.v. Web capture runs over
WebDriver-BiDi; native mobile capture is new — Android is passive (taps read
from adb getevent, hit-tested against the live accessibility tree), iOS is an
assisted REPL. Both synthesize semantic get_by_* locators rather than brittle
coordinates. Verified live on the Android emulator (5/5 synthesized selectors
re-resolved on-device). See CODEGEN_HANDOFF.md.
📱 Native mobile automation — a new vebidor.mobile module that drives
real apps on iOS (via WebDriverAgent) and Android (via the UiAutomator2 server)
directly over their HTTP sockets — the same backends Appium uses, with no Node middleware.
Sibling to vebidor.webdriver; shares its HttpTransport, lazy auto-waiting locator, and
poll_until_true assertion engine.
launch_ios(.simulator) / launch_android(.spawn) boot the
Simulator/emulator, spawn the backend, port-forward, poll readiness, and tear it all
down on close()get_by_role / get_by_label / get_by_text /
get_by_test_id compile to XCUITest predicates (iOS) and UiAutomator selectors (Android)tap / fill / swipe_* /
long_press / drag_to / scroll_into_viewmobile.expect(loc).to_be_visible() with .not() / .with_timeout()activate_app / terminate_app / query_app_state,
lock / unlock, set_orientation, set_geolocation, set_network_conditionVerified live: full Android path on an Android Emulator (Pixel AVD, API 34); iOS sessions, selectors, assertions, and gestures on an iOS Simulator.
📱 Playwright-style mobile-web emulation (over WebDriver-BiDi):
emulate_device(ctx, 'iPhone 14') (9 built-in: iPhone, Pixel, Galaxy, iPad)emulate() — viewport + DPR + JS-visible UA + isMobile/hasTouchLocator.tap() (touch pointer)set_request_user_agent — server-side UA (HTTP header rewrite)set_locale / set_timezone / set_screen_orientationmobileEmulation capability BiDi doesn't expose (touch detection is emulated; tap synthesizes a click)(v4.1.0: per-context conveniences + capability probing; v4.0.1: partition-aware cookies + inline dispatch — see CHANGELOG.)
🎭 Playwright-style API + WebDriver-BiDi — verified live against headless Edge
get_by_role/text/label/placeholder/test_id, nth/first)expect(loc).to_be_visible() etc., polling, .not() / .with_timeout()launch(): auto-detect driver+browser, free port, teardown — no manual driver startroute/route_response, fulfill/abort/continue, on_authwait_for_event, on_cookie_changedcall_function, viewport emulationset_files), BiDi cookies, screenshots/PDF, Tracervebidor now offers Playwright-style ergonomics and WebDriver-BiDi coverage that meets or exceeds Selenium's, on a native V API. See COMPARISON_WITH_PLAYWRIGHT.md.
🐛 Multi-Browser Bug Fixes - All 4 browsers now fully functional!
new_session() helper), Safari W3C JSON tags, moved new_edge_driver() to edge.v, standardized parameter namesfind_edge_binary helper for automatic Edge binary detectionTotal: 40 methods added across all phases, raising feature parity from 55% to 100%! 🎉
🏆 100% FEATURE PARITY WITH SELENIUM WEBDRIVER ACHIEVED! 🏆
See individual phase documentation for detailed examples and usage.
v-webdriver/
├── webdriver/
│ ├── client.v # Core WebDriver client + Transport seam
│ ├── elements.v # Element finding and interaction
│ ├── locator.v # Lazy auto-waiting Locator
│ ├── selectors.v # get_by_* selector engines
│ ├── assertions.v # Web-first expect() assertions
│ ├── launcher.v # launch(): driver/browser lifecycle
│ ├── fixtures.v # v test fixtures (with_browser, etc.)
│ ├── script.v # JavaScript execution
│ ├── window.v # Window management
│ ├── cookies.v # Cookie operations (Classic)
│ ├── screenshot.v # Screenshot capture
│ ├── actions.v # Actions API
│ ├── frame.v # Frame switching
│ ├── wait.v # Wait utilities
│ ├── capabiities.v # Capabilities configuration
│ ├── types.v # Common types
│ ├── errors.v # Error handling
│ ├── bidi.v # WebDriver-BiDi transport (WebSocket)
│ ├── bidi_modules.v # BiDi browsingContext/script/log helpers
│ ├── bidi_network.v # BiDi network interception/mocking + auth
│ ├── bidi_context.v # BiDi user contexts, viewport, PDF, history
│ ├── bidi_script.v # BiDi preload scripts + call_function
│ ├── bidi_storage.v # BiDi cookies + cookieChanged
│ ├── bidi_screenshot.v # BiDi per-context screenshots
│ ├── bidi_dom.v # BiDi node handles + setFiles (upload)
│ ├── bidi_trace.v # Lightweight Tracer
│ ├── webdriver_test.v # Unit tests
│ └── quick_test.v # Quick smoke tests
├── examples/
│ └── webdriver_latency_bench.v # Latency benchmark tool
├── integration_test.v # Integration tests
└── README.md # This file
Vebidor now supports 4 major browsers:
| Browser | Driver | Default Port | Function |
|---|---|---|---|
| Edge | EdgeDriver | 9515 | new_edge_driver() |
| Chrome | ChromeDriver | 9515 | new_chrome_driver() |
| Firefox | GeckoDriver | 4444 | new_firefox_driver() |
| Safari | SafariDriver | 4445 | new_safari_driver() |
EdgeOptions / ChromeOptions:
edge_options: webdriver.EdgeOptions{
args: ['--headless=new', '--disable-gpu', '--no-sandbox']
binary: r'C:\Program Files\...\browser.exe'
extensions: ['extension1.crx', 'extension2.crx']
}
FirefoxOptions:
firefox_options: webdriver.FirefoxOptions{
args: ['-headless', '-private']
binary: r'C:\Program Files\Mozilla Firefox\firefox.exe'
prefs: {
'browser.download.folderList': json.Any(2)
'browser.download.dir': json.Any('/tmp/downloads')
}
profile: '/path/to/firefox/profile'
}
SafariOptions:
safari_options: webdriver.SafariOptions{
automatic_inspection: false // Disable Web Inspector auto-open
automatic_profiling: false // Disable profiling auto-start
}
All browsers support standard W3C capabilities like accept_insecure_certs, page_load_strategy, timeouts, and proxy settings.
Contributions are welcome! Now that 100% feature parity is achieved, focus areas include:
Vebidor has achieved 100% feature parity with Selenium's core functionality!
[Your License Here]
For issues, questions, or contributions:
Status: Production-ready for web automation (Playwright-style API + WebDriver-BiDi on top of 100% Selenium parity) and native mobile automation (iOS via WebDriverAgent + Android via UiAutomator2). 🎉
Version: 5.1.0 (adds a codegen recorder for web + native mobile — emits semantic get_by_* locators; on top of v5.0.0's vebidor.mobile and the unchanged web stack: Playwright-style Locators/assertions, launch(), WebDriver-BiDi, mobile emulation, 4-browser support)
Selenium-parity phases: ✅ Phase 1 | ✅ Phase 2 | ✅ Phase 3 | ✅ Phase 4 | ✅ Phase 5 | ✅ Phase 6 | ✅ Phase 7 | ✅ Phase 8
Playwright-parity roadmap: ✅ Phase 0 (transport seam) | ✅ Phase 1 (locators/assertions) | ✅ Phase 2 (launch) | ✅ Phase 3 (BiDi transport) | ✅ Phase 4 (BiDi features) | ✅ Phase 5 (tooling) | ✅ BiDi gap closure vs Selenium
Latest Update: 2026-06-03 - v5.1.0 codegen recorder for web + native mobile (a11y-tree → semantic get_by_* locators; mobile capture verified live on the Android emulator)
117 commits
V
100.0%
A V language implementation of the W3C WebDriver protocol for browser automation - Fast, safe, and Selenium-compatible
20
stars
117
commits
V
primary language
Jul 22, 2026
updated
A V language implementation of the W3C WebDriver protocol for browser automation.
Version 5.1.0 | Playwright-style API + WebDriver-BiDi + native iOS/Android + codegen | 100% Selenium parity | Production Ready
Beyond the raw W3C WebDriver methods, Vebidor ships a higher-level ergonomic layer: one-call browser launch, lazy auto-waiting Locators, semantic selector engines, and retrying web-first assertions.
import vebidor.webdriver
fn main() {
// Auto-detects the driver + browser, picks a free port, and tears
// everything down on close(). No manual "start chromedriver" step.
mut b := webdriver.launch_edge(webdriver.LaunchOptions{ headless: true })!
defer { b.close() }
b.goto('https://example.com')!
// Locators are lazy and auto-wait for the element to be actionable.
b.wd.get_by_role('link', 'More information...').click()!
// Web-first assertions poll until the condition holds (or time out).
webdriver.expect(b.wd.get_by_role('heading', '')).to_be_visible()!
}
Selector engines: get_by_role, get_by_text, get_by_label,
get_by_placeholder, get_by_test_id, plus wd.locator('css=...' | 'xpath=...')
with chaining (row.locator('td')) and nth(i) / first().
Auto-waiting actions: click, fill, type_text, clear wait for the
element to be attached, visible, enabled, and scrolled into view.
Assertions: to_be_visible / to_be_hidden / to_be_enabled /
to_be_disabled, to_have_text / to_contain_text / to_have_value /
to_have_attribute / to_have_count, each invertible via .not() and tunable
via .with_timeout(ms).
Launch with bidi: true to get a persistent WebSocket alongside the Classic
session, unlocking event-driven features Classic cannot offer — network
interception/mocking, console/network listeners, isolated contexts, and more.
mut b := webdriver.launch_edge(webdriver.LaunchOptions{ headless: true, bidi: true })!
defer { b.close() }
mut bidi := b.bidi()!
defer { bidi.close() }
ctx := bidi.first_context()!
// Mock a response (Playwright route.fulfill style).
bidi.route(fn (req webdriver.InterceptedRequest) {
if req.url.contains('/api') {
req.fulfill(200, 'application/json', '{"mocked":true}') or {}
} else {
req.continue_request() or {}
}
})!
bidi.add_preload_script('() => { window.__patched = true }')! // runs before page scripts
bidi.navigate(ctx, 'https://example.com')!
bidi.on_log(fn (e webdriver.LogEntry) { println('${e.level}: ${e.text}') })!
BiDi capabilities: request/response interception + mocking (route,
route_response, fulfill/abort/continue), HTTP auth (on_auth),
console/network events (on_log/on_request/on_response), wait_for_event,
isolated user contexts (with per-context proxy, geolocation,
permissions, and storageState session reuse), preload scripts +
call_function, viewport emulation, partition-aware cookies
(get_cookies(user_context: uc)) + on_cookie_changed, screenshots/PDF, file
upload (set_files), and a Tracer. Capability probing via status() /
supports(). Any unwrapped command/event is reachable via send/on (or
on_sync for inline observers).
Mobile emulation (Playwright-style): device presets
(emulate_device(ctx, 'iPhone 14'), 9 built-in), emulate() for viewport/DPR/UA/
touch flags, Locator.tap(), set_request_user_agent (server-side UA), and
set_locale/set_timezone/set_screen_orientation. (Real touch-event dispatch
needs a CDP/mobileEmulation capability BiDi doesn't expose; touch detection is
emulated.)
See COMPARISON_WITH_PLAYWRIGHT.md for the full Playwright/Selenium feature mapping.
Core Features:
launch(), lazy auto-waiting Locators, selector engines, and web-first assertions (see ⚡ Modern API)Phase 1 & 5 - Element Properties ✨ ✅ 100% Complete:
is_displayed), enabled state (is_enabled), selection state (is_selected)get_css_value) - colors, fonts, dimensions, spacingPhase 2 - Alert Handling ✨:
Phase 3 - Page Information ✨:
Phase 4 - Window & Waits ✨:
Phase 6 - Expected Conditions ✨:
Phase 7 - Advanced Actions ✨:
Phase 8 - Async JS & Shadow DOM ✨ ✅ 100% Complete:
Edge-Specific:
The fastest way to get vebidor onto V's module path:
v install vebidor
This pulls the module so import vebidor.webdriver and import vebidor.mobile
resolve from anywhere. To track main instead of a published release, clone and
symlink the checkout:
git clone https://github.com/quaesitor-scientiam/vebidor.git
cd vebidor
mkdir -p ~/.vmodules
ln -s "$(pwd)" ~/.vmodules/vebidor
# (Alternatively: `v install` from this directory.)
Tip: With the Modern API
launch(), you can skip the manual driver-start step below — Vebidor finds the driver onPATH(or viaEDGEDRIVER/CHROMEDRIVER/GECKODRIVER), starts it on a free port, and stops it for you. The steps below are for the classicnew_*_driver(url, caps)flow that connects to a driver you started yourself.
# Start your browser's WebDriver
# Edge/Chrome:
.\msedgedriver.exe --port=9515 # or chromedriver.exe
# Firefox:
.\geckodriver.exe --port=4444
# Safari (macOS):
safaridriver --enable
safaridriver -p 4445
Need help? See TEST_ENVIRONMENT_SETUP.md for detailed setup instructions.
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'msedge'
edge_options: webdriver.EdgeOptions{
args: ['--headless=new']
binary: r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
}
}
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
title := wd.get_title()!
println('Page title: ${title}')
}
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'chrome'
chrome_options: webdriver.ChromeOptions{
args: ['--headless=new', '--disable-gpu']
binary: r'C:\Program Files\Google\Chrome\Application\chrome.exe'
}
}
wd := webdriver.new_chrome_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
println('Chrome automation working!')
}
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'firefox'
firefox_options: webdriver.FirefoxOptions{
args: ['-headless']
binary: r'C:\Program Files\Mozilla Firefox\firefox.exe'
}
}
wd := webdriver.new_firefox_driver('http://127.0.0.1:4444', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
println('Firefox automation working!')
}
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'safari'
safari_options: webdriver.SafariOptions{
automatic_inspection: false
automatic_profiling: false
}
}
wd := webdriver.new_safari_driver('http://127.0.0.1:4445', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
println('Safari automation working!')
}
import vebidor.webdriver
fn main() {
caps := webdriver.Capabilities{
browser_name: 'msedge'
edge_options: webdriver.EdgeOptions{
args: ['--headless=new']
}
}
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
// Set timeouts (Phase 4)
wd.set_implicit_wait(10000)!
wd.set_page_load_timeout(30000)!
// Navigate and get page info (Phase 3)
wd.get('https://example.com')!
title := wd.get_title()!
url := wd.get_current_url()!
println('Page: ${title} at ${url}')
// Element properties (Phase 1)
heading := wd.find_element('css selector', 'h1')!
text := wd.get_text(heading)!
tag := wd.get_tag_name(heading)!
visible := wd.is_displayed(heading)!
println('Found <${tag}>: "${text}" (visible: ${visible})')
// Handle alerts (Phase 2)
wd.execute_script('alert("Test")', [])!
alert_text := wd.get_alert_text()!
println('Alert says: ${alert_text}')
wd.accept_alert()!
// Multi-window (Phase 4)
new_tab := wd.new_window('tab')!
wd.switch_to_window(new_tab.handle)!
wd.get('https://www.iana.org')!
wd.maximize_window()!
}
import vebidor.webdriver
fn login_example() ! {
caps := webdriver.Capabilities{
browser_name: 'msedge'
}
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com/login')!
// Find form elements
username := wd.find_element('css selector', '#username')!
password := wd.find_element('css selector', '#password')!
submit := wd.find_element('css selector', 'button[type="submit"]')!
// Fill the form
wd.send_keys(username, 'testuser')!
wd.send_keys(password, 'password123')!
wd.click(submit)!
}
import vebidor.webdriver
fn cookie_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Add a cookie
cookie := webdriver.Cookie{
name: 'session_id'
value: 'abc123'
path: '/'
domain: 'example.com'
}
wd.add_cookie(cookie)!
// Get all cookies
cookies := wd.get_cookies()!
println('Cookies: ${cookies.len}')
// Delete a cookie
wd.delete_cookie('session_id')!
}
import vebidor.webdriver
fn actions_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Type text using actions
wd.type_text('Hello World')!
// Scroll down
wd.scroll_by(0, 500)!
// Click at coordinates
wd.click_at(100, 200)!
}
import vebidor.webdriver
fn element_properties_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
link := wd.find_element('css selector', 'a')!
// Get element properties
text := wd.get_text(link)! // Visible text
href := wd.get_attribute(link, 'href')! // HTML attribute
tag := wd.get_tag_name(link)! // Tag name
// Check element state
visible := wd.is_displayed(link)!
enabled := wd.is_enabled(link)!
println('Link: ${text} -> ${href}')
println('Visible: ${visible}, Enabled: ${enabled}')
}
import vebidor.webdriver
fn alert_handling_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Handle alert
wd.execute_script('alert("Hello!")', [])!
text := wd.get_alert_text()!
println('Alert: ${text}')
wd.accept_alert()!
// Handle prompt
wd.execute_script('window.name = prompt("Your name?")', [])!
wd.send_alert_text('Claude')!
wd.accept_alert()!
}
import vebidor.webdriver
fn page_info_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Get page metadata
title := wd.get_title()!
url := wd.get_current_url()!
source := wd.get_page_source()!
println('Title: ${title}')
println('URL: ${url}')
println('HTML length: ${source.len} bytes')
// Verify navigation
assert title == 'Example Domain'
assert url == 'https://example.com/'
}
import vebidor.webdriver
fn multi_window_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
wd.get('https://example.com')!
// Get current window
main_window := wd.get_window_handle()!
// Create new tab
new_tab := wd.new_window('tab')!
wd.switch_to_window(new_tab.handle)!
// Navigate in new tab
wd.get('https://www.iana.org')!
new_title := wd.get_title()!
// Switch back to main window
wd.switch_to_window(main_window)!
// Window state management
wd.maximize_window()!
wd.fullscreen_window()!
}
import vebidor.webdriver
fn timeouts_example() ! {
caps := webdriver.Capabilities{ browser_name: 'msedge' }
wd := webdriver.new_edge_driver('http://127.0.0.1:9515', caps)!
defer { wd.quit() or {} }
// Configure timeouts
wd.set_implicit_wait(10000)! // Auto-wait 10s for elements
wd.set_page_load_timeout(30000)! // 30s page load timeout
wd.set_script_timeout(15000)! // 15s script timeout
wd.get('https://example.com')!
// Now element finding automatically waits
element := wd.find_element('css selector', 'h1')!
// Custom wait condition
wd.wait_for(fn (wd webdriver.WebDriver) !bool {
title := wd.get_title()!
return title.len > 0
}, 5000, 500)!
}
v test webdriver/quick_test.v
v test webdriver/
v run integration_test.v
v simple_test.v
.\simple_test.exe
examples/webdriver_latency_bench.v measures round-trip latency for common WebDriver commands (get_title, get_current_url, execute_script) and reports min/avg/p50/p95/p99/max in milliseconds.
Prerequisites: a WebDriver must already be running before launching the benchmark.
# Default (stdlib net.http)
v run examples/webdriver_latency_bench.v
# Raw TCP transport (opt-in, avoids keep-alive stall)
v -d wd_use_raw_tcp run examples/webdriver_latency_bench.v
Options:
| Flag | Default | Description |
|---|---|---|
--browser=edge|chrome|firefox|safari | edge | Browser to use |
--driver=URL | http://127.0.0.1:9515 | WebDriver URL |
--iters=N | 200 | Number of timed iterations |
--warmup=N | 20 | Warmup iterations (not measured) |
--binary=PATH | (env var) | Path to browser executable |
--headless / --headed | headless | Run browser headlessly or not |
Example output:
Command: get_title
count: 200 min: 2.007 ms avg: 2.460 ms p50: 2.344 ms p95: 3.045 ms p99: 4.023 ms max: 4.545 ms
Beyond browser automation, Vebidor ships a native mobile module
(vebidor.mobile) that drives real apps on iOS and Android — by
talking directly to WebDriverAgent (iOS) and the UiAutomator2 server
(Android), the same on-device backends Appium uses, but with no Node
middleware.
import vebidor.mobile
mut s := mobile.launch_android(mobile.AndroidOptions{
mode: .spawn
udid: 'emulator-5554'
app_package: 'com.android.settings'
app_activity: '.Settings'
uia2_server_apk: server_apk
uia2_server_test_apk: test_apk
})!
defer { s.close() }
// Same Playwright-style surface as the web module:
mobile.expect(s.get_by_text('Battery')).to_be_visible()!
s.get_by_label('Battery').tap()!
s.set_orientation(.landscape)!
One API, two backends. Cross-platform get_by_label/text/test_id/role
selectors, lazy auto-waiting locators, expect() assertions, touch
gestures, app lifecycle, and device state (orientation, lock, geolocation,
network) — all sharing the web module's transport and polling engine. The
Android path is verified live on an emulator; iOS on a Simulator.
See COMPARISON_WITH_APPIUM.md for the Appium feature mapping and MOBILE_TESTING.md for setup.
Current: 🎉 100% feature parity with Selenium WebDriver! 🎉 ALL Phases Complete! 🎊
| Category | Status |
|---|---|
| Session Management | ✅ 100% |
| Navigation | ✅ 100% |
| Element Location | ✅ 100% |
| Cookies | ✅ 100% |
| Screenshots | ✅ 100% |
| Frames | ✅ 100% |
| Element Properties | ✅ 100% (Phase 1, 5 & 7) |
| Alerts | ✅ 100% (Phase 2) |
| Page Information | ✅ 100% (Phase 3) |
| Window Management | ✅ 100% (Phase 4) |
| Timeouts & Waits | ✅ 100% (Phase 4 & 6) |
| Actions API | ✅ 100% (Phase 7) |
| Element Interaction | ✅ 100% (Phase 7) |
| JavaScript Execution | ✅ 100% (Phase 8) ✨ NEW |
| Shadow DOM | ✅ 100% (Phase 8) ✨ NEW |
🏆 100% FEATURE PARITY ACHIEVED! 🏆
See COMPARISON_WITH_PLAYWRIGHT.md for the full Playwright/Selenium feature mapping.
🎬 Codegen / session recorder — record a live session and emit runnable
vebidor source via tools/codegen.v. Web capture runs over
WebDriver-BiDi; native mobile capture is new — Android is passive (taps read
from adb getevent, hit-tested against the live accessibility tree), iOS is an
assisted REPL. Both synthesize semantic get_by_* locators rather than brittle
coordinates. Verified live on the Android emulator (5/5 synthesized selectors
re-resolved on-device). See CODEGEN_HANDOFF.md.
📱 Native mobile automation — a new vebidor.mobile module that drives
real apps on iOS (via WebDriverAgent) and Android (via the UiAutomator2 server)
directly over their HTTP sockets — the same backends Appium uses, with no Node middleware.
Sibling to vebidor.webdriver; shares its HttpTransport, lazy auto-waiting locator, and
poll_until_true assertion engine.
launch_ios(.simulator) / launch_android(.spawn) boot the
Simulator/emulator, spawn the backend, port-forward, poll readiness, and tear it all
down on close()get_by_role / get_by_label / get_by_text /
get_by_test_id compile to XCUITest predicates (iOS) and UiAutomator selectors (Android)tap / fill / swipe_* /
long_press / drag_to / scroll_into_viewmobile.expect(loc).to_be_visible() with .not() / .with_timeout()activate_app / terminate_app / query_app_state,
lock / unlock, set_orientation, set_geolocation, set_network_conditionVerified live: full Android path on an Android Emulator (Pixel AVD, API 34); iOS sessions, selectors, assertions, and gestures on an iOS Simulator.
📱 Playwright-style mobile-web emulation (over WebDriver-BiDi):
emulate_device(ctx, 'iPhone 14') (9 built-in: iPhone, Pixel, Galaxy, iPad)emulate() — viewport + DPR + JS-visible UA + isMobile/hasTouchLocator.tap() (touch pointer)set_request_user_agent — server-side UA (HTTP header rewrite)set_locale / set_timezone / set_screen_orientationmobileEmulation capability BiDi doesn't expose (touch detection is emulated; tap synthesizes a click)(v4.1.0: per-context conveniences + capability probing; v4.0.1: partition-aware cookies + inline dispatch — see CHANGELOG.)
🎭 Playwright-style API + WebDriver-BiDi — verified live against headless Edge
get_by_role/text/label/placeholder/test_id, nth/first)expect(loc).to_be_visible() etc., polling, .not() / .with_timeout()launch(): auto-detect driver+browser, free port, teardown — no manual driver startroute/route_response, fulfill/abort/continue, on_authwait_for_event, on_cookie_changedcall_function, viewport emulationset_files), BiDi cookies, screenshots/PDF, Tracervebidor now offers Playwright-style ergonomics and WebDriver-BiDi coverage that meets or exceeds Selenium's, on a native V API. See COMPARISON_WITH_PLAYWRIGHT.md.
🐛 Multi-Browser Bug Fixes - All 4 browsers now fully functional!
new_session() helper), Safari W3C JSON tags, moved new_edge_driver() to edge.v, standardized parameter namesfind_edge_binary helper for automatic Edge binary detectionTotal: 40 methods added across all phases, raising feature parity from 55% to 100%! 🎉
🏆 100% FEATURE PARITY WITH SELENIUM WEBDRIVER ACHIEVED! 🏆
See individual phase documentation for detailed examples and usage.
v-webdriver/
├── webdriver/
│ ├── client.v # Core WebDriver client + Transport seam
│ ├── elements.v # Element finding and interaction
│ ├── locator.v # Lazy auto-waiting Locator
│ ├── selectors.v # get_by_* selector engines
│ ├── assertions.v # Web-first expect() assertions
│ ├── launcher.v # launch(): driver/browser lifecycle
│ ├── fixtures.v # v test fixtures (with_browser, etc.)
│ ├── script.v # JavaScript execution
│ ├── window.v # Window management
│ ├── cookies.v # Cookie operations (Classic)
│ ├── screenshot.v # Screenshot capture
│ ├── actions.v # Actions API
│ ├── frame.v # Frame switching
│ ├── wait.v # Wait utilities
│ ├── capabiities.v # Capabilities configuration
│ ├── types.v # Common types
│ ├── errors.v # Error handling
│ ├── bidi.v # WebDriver-BiDi transport (WebSocket)
│ ├── bidi_modules.v # BiDi browsingContext/script/log helpers
│ ├── bidi_network.v # BiDi network interception/mocking + auth
│ ├── bidi_context.v # BiDi user contexts, viewport, PDF, history
│ ├── bidi_script.v # BiDi preload scripts + call_function
│ ├── bidi_storage.v # BiDi cookies + cookieChanged
│ ├── bidi_screenshot.v # BiDi per-context screenshots
│ ├── bidi_dom.v # BiDi node handles + setFiles (upload)
│ ├── bidi_trace.v # Lightweight Tracer
│ ├── webdriver_test.v # Unit tests
│ └── quick_test.v # Quick smoke tests
├── examples/
│ └── webdriver_latency_bench.v # Latency benchmark tool
├── integration_test.v # Integration tests
└── README.md # This file
Vebidor now supports 4 major browsers:
| Browser | Driver | Default Port | Function |
|---|---|---|---|
| Edge | EdgeDriver | 9515 | new_edge_driver() |
| Chrome | ChromeDriver | 9515 | new_chrome_driver() |
| Firefox | GeckoDriver | 4444 | new_firefox_driver() |
| Safari | SafariDriver | 4445 | new_safari_driver() |
EdgeOptions / ChromeOptions:
edge_options: webdriver.EdgeOptions{
args: ['--headless=new', '--disable-gpu', '--no-sandbox']
binary: r'C:\Program Files\...\browser.exe'
extensions: ['extension1.crx', 'extension2.crx']
}
FirefoxOptions:
firefox_options: webdriver.FirefoxOptions{
args: ['-headless', '-private']
binary: r'C:\Program Files\Mozilla Firefox\firefox.exe'
prefs: {
'browser.download.folderList': json.Any(2)
'browser.download.dir': json.Any('/tmp/downloads')
}
profile: '/path/to/firefox/profile'
}
SafariOptions:
safari_options: webdriver.SafariOptions{
automatic_inspection: false // Disable Web Inspector auto-open
automatic_profiling: false // Disable profiling auto-start
}
All browsers support standard W3C capabilities like accept_insecure_certs, page_load_strategy, timeouts, and proxy settings.
Contributions are welcome! Now that 100% feature parity is achieved, focus areas include:
Vebidor has achieved 100% feature parity with Selenium's core functionality!
[Your License Here]
For issues, questions, or contributions:
Status: Production-ready for web automation (Playwright-style API + WebDriver-BiDi on top of 100% Selenium parity) and native mobile automation (iOS via WebDriverAgent + Android via UiAutomator2). 🎉
Version: 5.1.0 (adds a codegen recorder for web + native mobile — emits semantic get_by_* locators; on top of v5.0.0's vebidor.mobile and the unchanged web stack: Playwright-style Locators/assertions, launch(), WebDriver-BiDi, mobile emulation, 4-browser support)
Selenium-parity phases: ✅ Phase 1 | ✅ Phase 2 | ✅ Phase 3 | ✅ Phase 4 | ✅ Phase 5 | ✅ Phase 6 | ✅ Phase 7 | ✅ Phase 8
Playwright-parity roadmap: ✅ Phase 0 (transport seam) | ✅ Phase 1 (locators/assertions) | ✅ Phase 2 (launch) | ✅ Phase 3 (BiDi transport) | ✅ Phase 4 (BiDi features) | ✅ Phase 5 (tooling) | ✅ BiDi gap closure vs Selenium
Latest Update: 2026-06-03 - v5.1.0 codegen recorder for web + native mobile (a11y-tree → semantic get_by_* locators; mobile capture verified live on the Android emulator)
117 commits
V
100.0%