Cross-platform web automation: Selenium + Playwright, plus a JSON-driven action executor with batteries included.
WebRunner (je_web_runner) started as a Selenium wrapper and grew into a full automation platform: a Selenium and a Playwright backend behind one JSON-driven action executor, plus modules for reporting, observability, orchestration, security, and AI assistance. Every executor command has a deterministic name (WR_*) and a single dispatch point, so an action JSON can mix browser, HTTP, database, and webhook calls in the same script.
Auto-generated reference — every registered
WR_*command (signature + summary) is exported underdocs/reference/command_reference.md, and a JSON Schema for action JSON files lives atdocs/reference/webrunner-action-schema.json.
WR_pw_* and is fully opt-in.Executor.event_dict; legacy aliases stay alongside snake_case names for back-compat, and a JSON Schema is exported for IDE autocomplete.set_driver(experimental_options=, extension_paths=, enable_bidi=), attach_to_existing_browser, native CDP shortcuts (set_timezone / set_locale / set_device_metrics / set_user_agent / set_extra_http_headers / set_geolocation / set_network_conditions / block_urls / set_cache_disabled / set_download_directory), Fetch interception primitives (enable_fetch_interception / continue_request / fulfill_request / fail_request), W3C BiDi listeners (add_console_listener / add_js_error_listener), save_cookies / load_cookies for session reuse, save_full_page_screenshot, print_page (PDF), reload(ignore_cache), bring_to_front, switch_to_window_by_url|title, page metadata getters (get_current_url / get_title / get_page_source / get_window_handles / new_window / close_window). All exposed via WR_* aliases too.CDPEventListener (WebSocket loop + sync send / on / context manager), record_trace(driver, path) for Chrome DevTools-loadable performance traces, and a bidi_network module wrapping driver.network.add_request_handler / add_response_handler / add_auth_handler for cross-browser request interception..env loader with ${ENV.X} placeholder expansion, CSV/JSON data-driven runner with ${ROW.x}.Stable:
pip install je_web_runner
Development:
pip install je_web_runner_dev
Optional dependencies (each enables a slice of features; install only what you use):
pip install playwright # Playwright backend
python -m playwright install # Browser binaries for Playwright
pip install Pillow # Visual regression
pip install faker # Random test data (WR_faker_*)
pip install sqlalchemy # Database validation (WR_db_*)
pip install opentelemetry-sdk # Distributed traces (WR_set_action_span_factory)
pip install Appium-Python-Client # Mobile native (WR_appium_*)
pip install testcontainers # Spin up Postgres / Redis (WR_tc_*)
pip install locust # Load testing (WR_locust_*)
Hard requirements: Python 3.10+, selenium>=4.0.0, requests, python-dotenv, webdriver-manager, defusedxml, Pillow.
flowchart LR
subgraph Authoring
A1["Action JSON files"]
A2["Programmatic Python API"]
A3["Browser recorder<br/>(JS injection)"]
A4["LLM NL → action draft"]
end
subgraph Core
EXE["Executor<br/>event_dict"]
REC["Test record<br/>singleton"]
LDG["Run ledger /<br/>flaky detection"]
end
subgraph Backends
SEL["Selenium<br/>WebDriverWrapper"]
PW["Playwright<br/>PlaywrightWrapper"]
APM["Appium<br/>Mobile"]
HTTP["HTTP API<br/>requests"]
DB["Database<br/>SQLAlchemy"]
end
subgraph Outputs
REP["Reports<br/>HTML/JSON/XML/JUnit/Allure"]
OBS["Observability<br/>OTel · dashboard · replay"]
NOT["Notifiers<br/>Slack · webhook · GH · JIRA · TestRail"]
end
A1 --> EXE
A2 --> EXE
A3 --> A1
A4 --> A1
EXE --> SEL
EXE --> PW
EXE --> APM
EXE --> HTTP
EXE --> DB
SEL --> REC
PW --> REC
APM --> REC
HTTP --> REC
DB --> REC
REC --> LDG
REC --> REP
REC --> OBS
REC --> NOT
flowchart LR
IN["Action<br/>[cmd, args, kwargs]"] --> VAL["JSON validator<br/>(WR_validate_*)"]
VAL --> ENV["${ENV.X} / ${ROW.x}<br/>placeholder expansion"]
ENV --> SPAN["OTel span factory<br/>(optional)"]
SPAN --> RETRY["Retry policy<br/>retries × backoff"]
RETRY --> GATE["Arbitrary-script<br/>gate"]
GATE --> DISP["event_dict[cmd](*args, **kwargs)"]
DISP --> RECORD["test_record_instance<br/>append()"]
DISP -- failure --> SHOT["Auto-screenshot<br/>(failure dir)"]
RECORD --> DONE["Result dict"]
SHOT --> DONE
flowchart TB
CMD["Action command name"] --> ROUTE{"prefix?"}
ROUTE -- "WR_pw_*" --> PW["Playwright backend<br/>(PlaywrightWrapper)"]
ROUTE -- "WR_pw_element_*" --> PWE["Playwright element<br/>(PlaywrightElementWrapper)"]
ROUTE -- "WR_appium_*" --> APM["Appium driver"]
ROUTE -- "WR_http_*" --> HTTP["requests wrapper"]
ROUTE -- "WR_db_*" --> DB["SQLAlchemy validator"]
ROUTE -- "WR_pw_a11y_* / WR_a11y_*" --> AXE["axe-core audit"]
ROUTE -- "WR_pw_throttle / WR_throttle" --> THR["Network throttling<br/>(CDP)"]
ROUTE -- "WR_pw_route_*" --> ROUTE_MOCK["Playwright route mock"]
ROUTE -- "WR_*<br/>(default)" --> SEL["Selenium backend<br/>(WebDriverWrapper)"]
ROUTE -- "WR_element_*<br/>(default)" --> SE["Selenium element<br/>(WebElementWrapper)"]
je_web_runner/
├── __init__.py
├── __main__.py # CLI: --execute_dir / --watch / --tag / --shard / --migrate ...
├── element/web_element_wrapper.py
├── manager/webrunner_manager.py
├── webdriver/
│ ├── webdriver_wrapper.py # Selenium core
│ ├── webdriver_with_options.py
│ ├── playwright_wrapper.py # Playwright sync backend (full)
│ ├── playwright_element_wrapper.py
│ └── playwright_locator.py # TestObject ↔ Playwright selector
└── utils/
├── ab_run/ # A/B run mode (run_ab + diff_records)
├── accessibility/ # axe-core audit
├── ai_assist/ # Pluggable LLM scaffold
├── api/ # HTTP API testing commands
├── appium_integration/ # Mobile native via Appium
├── auth/ # OAuth2 / OIDC token helpers
├── callback/ # Callback executor
├── cdp/ # Raw CDP passthrough
├── ci_annotations/ # GitHub Actions ::error::
├── cli/ # CLI parser, watch mode, dispatch
├── cloud_grid/ # BrowserStack / Sauce Labs / LambdaTest
├── dashboard/ # Live progress HTTP server
├── database/ # SQL validation (SQLAlchemy)
├── data_driven/ # CSV/JSON dataset + ${ROW.x}
├── docs/ # Auto-generated command reference
├── dom_traversal/ # Shadow DOM / iframe helpers
├── env_config/ # .env loader + ${ENV.X}
├── exception/ # Exception hierarchy
├── executor/ # Action executor + retry/screenshot/gate
├── extensions/ # Browser extension loaders
├── factories/ # Factory pattern helpers
├── file_process/ # File utilities
├── file_transfer/ # Upload / download helpers
├── generate_report/ # HTML/JSON/XML/JUnit/Allure + manifest
├── har_diff/ # HAR file diff
├── json/ # JSON I/O + validator (length 1/2/3)
├── lighthouse/ # Lighthouse CLI runner
├── linter/ # action_linter + migration
├── load_test/ # Locust wrapper
├── logging/ # Rotating file handler
├── multi_user/ # Multi-user matrix runner
├── network_emulation/ # Throttling presets via CDP
├── notifier/ # Slack / generic webhooks
├── observability/ # Console+network capture · OTel
├── package_manager/ # Dynamic plugin loader
├── perf_metrics/ # FCP / LCP / CLS / TTFB
├── pom_generator/ # POM skeleton from URL/HTML
├── project/ # Project template generator
├── recorder/ # JS-injection recorder + PII mask
├── replay_studio/ # HTML timeline studio
├── run_ledger/ # ledger · flaky · classifier
├── schema/ # Action JSON Schema export
├── scheduler/ # stdlib-sched scheduled runner
├── secrets_scanner/ # Hard-coded credential scanner
├── security_headers/ # HTTP headers audit
├── selenium_utils_wrapper/ # Keys / Capabilities
├── self_healing/ # Fallback locator registry
├── service_worker/ # SW unregister + cache clear
├── sharding/ # Deterministic test sharding
├── snapshot/ # Text/DOM snapshot testing
├── socket_server/ # TCP server with token + TLS
├── storage/ # localStorage / session / IDB
├── test_data/ # Faker integration
├── test_filter/ # Tag filter + dependency graph
├── test_management/ # JIRA + TestRail
├── test_object/ # TestObject + record
├── test_record/ # Action recording
├── testcontainers_integration/ # Postgres / Redis / generic
├── visual_regression/ # Pillow-based image diff
└── xml/ # XML utilities
The examples/ directory ships runnable recipes that exercise the new
helpers against real Chrome / network. Each is invoked from the repo root:
| Example | Demonstrates |
|---|---|
counting_stars.{py,json} |
WR_sleep, WR_set_driver with Chrome flags, autoplay-policy override, JS-driven video.play(), skip-ad polling. |
google_search.py |
Consent dismissal, search-box typing, ENTER submit, result heading scrape. |
form_submit.py |
form_autofill.plan_fill_actions + state_diff.capture_state round trip against httpbin/forms/post. |
smart_wait_demo.py |
wait_for_fetch_idle + wait_for_spa_route_stable + memory_leak.detect_growth against a real page. |
fanout_demo.py |
fanout.run_fan_out parallel HTTP preflights. |
pii_redact_demo.py |
pii_scanner.scan_text + redact_text + assert_no_pii (pure logic). |
quick_smoke.json |
Minimal WR_set_driver → WR_sleep → WR_execute_script → WR_quit_all smoke via the executor CLI. |
Run a Python example directly:
python examples/google_search.py
Run an action JSON example through the executor:
python -m je_web_runner -e examples/quick_smoke.json
test/
├── unit_test/ # 1200 mock-based unit tests (~12s)
├── integration_test/ # 30 wired-modules tests with real I/O (~6s)
└── e2e_test/ # 6 real-browser tests; skips without Selenium Grid
test/unit_test/test_*.py) — runs everywhere; pulled in by both
test_dev.yml and test_stable.yml.test/integration_test/) — wires 2+ modules together
with real SQLite, in-process HTTP servers, and real subprocesses for
the MCP / LSP. Same workflows as unit, second step.test/e2e_test/) — talks to a Selenium Grid via
WEBRUNNER_E2E_HUB. Locally: cd docker && docker compose up -d.
CI: .github/workflows/e2e_browser.yml boots selenium/hub:4.20.0
selenium/node-chrome daily / on demand.The 80+ utility helpers live under je_web_runner.utils.<area>; for
discoverability they are also re-exported under je_web_runner.api:
from je_web_runner.api import (
authoring, # action_formatter, md_authoring, templates, sel_to_pw, bootstrap
debugging, # cross_browser, pr_comment, extension_harness
frontend, # device emulation, geo/locale, multi-tab, shadow pierce, …
infra, # driver pin, k8s runner, pipeline, lock, watch_mode, …
mobile, # Appium gestures
networking, # api_mock, contract_testing, GraphQL, mock services, har_replay
observability, # timeline, failure bundle, trace recorder, OTLP, BiDi, cdp_tap
quality, # a11y_diff, a11y_trend, perf budgets/drift, trend, failure cluster
reliability, # adaptive retry, browser pool, smart wait, throttler, supervisor
security, # PII, license, CSP, cookie consent, header tampering
test_data, # DB fixtures, fixture record/replay, form auto-fill
)
The original Selenium-flavoured top-level surface (webdriver_wrapper_instance,
execute_action, TestObject, …) is unchanged.
from je_web_runner import TestObject, get_webdriver_manager, web_element_wrapper
manager = get_webdriver_manager("chrome")
manager.webdriver_wrapper.to_url("https://www.google.com")
manager.webdriver_wrapper.implicitly_wait(2)
search_box = TestObject("q", "name")
manager.webdriver_wrapper.find_element(search_box)
web_element_wrapper.click_element()
web_element_wrapper.input_to_element("WebRunner automation")
manager.quit()
from je_web_runner import execute_action
actions = [
["WR_new_driver", {"webdriver_name": "chrome"}],
["WR_to_url", {"url": "https://www.google.com"}],
["WR_implicitly_wait", {"time_to_wait": 2}],
["WR_save_test_object", {"test_object_name": "q", "object_type": "NAME"}],
["WR_find_recorded_element", {"element_name": "q"}],
["WR_element_click"],
["WR_element_input", {"input_value": "WebRunner automation"}],
["WR_quit_all"],
]
execute_action(actions)
The legacy names (WR_get_webdriver_manager, WR_SaveTestObject, WR_quit, WR_input_to_element, …) still work — see Quality & Security for the one-shot migration helper.
[
["WR_to_url", ["https://example.com"], {"timeout": 30}],
]
The validator accepts length-1, length-2 ([cmd, dict_or_list]), and length-3 ([cmd, [positional], {kwargs}]) actions.
The original Selenium-flavoured API remains the canonical entry point for programmatic use. Sections preserved from the original README:
get_webdriver_manager, new_driver, change_webdriver, close_choose_webdriver, quit.to_url, forward, back, refresh, find_element, find_elements, implicitly_wait, explict_wait (alias WR_explicit_wait), set_script_timeout, set_page_load_timeout, the full ActionChains-backed mouse/keyboard surface, cookies, execute_script, window management, screenshots, frame/window/alert switching, get_log.click_element, input_to_element, clear, submit, get_attribute, get_property, get_dom_attribute, is_displayed, is_enabled, is_selected, value_of_css_property, screenshot, change_web_element, check_current_web_element, plus the new select_by_value / select_by_index / select_by_visible_text.TestObject(name, type), create_test_object, get_test_object_type_list (returns ['ID', 'NAME', 'XPATH', 'CSS_SELECTOR', 'CLASS_NAME', 'TAG_NAME', 'LINK_TEXT', 'PARTIAL_LINK_TEXT']).Programmatic examples for each surface are kept identical to the previous edition; see the relevant Sphinx pages under docs/source/Eng/doc/ for full code snippets.
The executor maps a string command name to a Python callable. Every backend, integration, and helper registers under event_dict.
["command"] # no args
["command", {"key": "value"}] # kwargs
["command", [arg1, arg2]] # positional
["command", [arg1], {"key": "value"}] # positional + kwargs (length 3)
[
["WR_pw_evaluate", ["() => document.title"], {"arg": None}],
]
WR_sleep blocks the executor thread for a given number of seconds — useful when the page needs settle time, when a JS animation needs to finish, or when an example wants to hold the browser open for the user to watch:
[
["WR_to_url", {"url": "https://example.com"}],
["WR_sleep", {"seconds": 2.5}],
["WR_get_screenshot_as_png"],
]
Negative or non-numeric seconds raise ValueError. For pacing inside JavaScript (e.g. waiting on a custom event from the page) use WR_execute_async_script with a setTimeout-driven callback.
[ ...actions... ] # bare list
{
"webdriver_wrapper": [ ...actions... ],
"meta": {"tags": ["smoke", "fast"], "depends_on": ["login"]} # optional
}
meta.tags and meta.depends_on are picked up by the CLI for filtering and topological execution.
from je_web_runner import add_command_to_executor
def my_step(name: str) -> None:
print(f"hello {name}")
add_command_to_executor({"my_command": my_step})
from je_web_runner.utils.executor.action_executor import executor
executor.set_retry_policy(retries=2, backoff=0.5) # global retry
executor.set_failure_screenshot_dir("./failures") # auto PNG on raise
executor.set_allow_arbitrary_script(False) # gate WR_execute_script / WR_pw_evaluate / WR_cdp
Selenium is the original backend. Every legacy command (and its modern alias) routes here unless an explicit WR_pw_* / WR_appium_* prefix is used.
The Playwright backend mirrors the operational surface of the Selenium wrapper under WR_pw_*:
WR_pw_launch, WR_pw_quit, WR_pw_new_page, WR_pw_switch_to_page, WR_pw_close_page, WR_pw_to_url, WR_pw_forward, WR_pw_back, WR_pw_refresh, WR_pw_url, WR_pw_title, WR_pw_content.WR_pw_find_element, WR_pw_find_elements, WR_pw_find_element_with_test_object_record, WR_pw_find_with_healing.WR_pw_click, WR_pw_dblclick, WR_pw_hover, WR_pw_fill, WR_pw_type_text, WR_pw_press, WR_pw_check, WR_pw_uncheck, WR_pw_select_option, WR_pw_drag_and_drop.WR_pw_find_element_with_test_object_record) — WR_pw_element_click, WR_pw_element_dblclick, WR_pw_element_fill, WR_pw_element_type_text, WR_pw_element_press, WR_pw_element_check, WR_pw_element_uncheck, WR_pw_element_select_option, WR_pw_element_get_attribute, WR_pw_element_inner_text, WR_pw_element_inner_html, WR_pw_element_is_visible, WR_pw_element_is_enabled, WR_pw_element_is_checked, WR_pw_element_scroll_into_view, WR_pw_element_screenshot, WR_pw_element_change.WR_pw_evaluate, WR_pw_get_cookies, WR_pw_add_cookies, WR_pw_clear_cookies, WR_pw_screenshot, WR_pw_wait_for_selector, WR_pw_wait_for_load_state, WR_pw_wait_for_timeout, WR_pw_wait_for_url, WR_pw_set_viewport_size, WR_pw_mouse_*, WR_pw_keyboard_*.WR_pw_emulate("iPhone 13"), WR_pw_set_locale, WR_pw_set_timezone, WR_pw_clock_install / _set_time / _run_for, WR_pw_set_geolocation, WR_pw_grant_permissions.WR_pw_start_har_recording, WR_pw_stop_har_recording, WR_pw_route_mock, WR_pw_route_mock_json, WR_pw_route_unmock, WR_pw_route_clear.Existing scripts can move to Playwright incrementally; TestObject records are translated to Playwright selectors automatically (CSS_SELECTOR → as-is, XPATH → xpath=…, ID → #…, NAME → [name="…"], LINK_TEXT → text=…, PARTIAL_LINK_TEXT → :has-text("…")).
from je_web_runner import (
connect_browserstack,
build_browserstack_capabilities,
)
connect_browserstack(
username="...",
access_key="...",
capabilities=build_browserstack_capabilities(
browser_name="chrome",
browser_version="latest",
os_name="Windows",
os_version="11",
project="WebRunner",
build="ci-2026-04-26",
),
)
# All existing WR_* commands now run against the cloud session.
connect_saucelabs and connect_lambdatest follow the same shape.
from je_web_runner import (
start_appium_session,
build_android_caps,
build_ios_caps,
)
start_appium_session(
"https://appium.example/wd/hub",
capabilities=build_android_caps(app="/path/to/app.apk"),
)
# WR_* commands now drive the mobile session.
from je_web_runner import (
generate_html_report,
generate_json_report,
generate_xml_report,
generate_junit_xml_report,
generate_allure_report,
)
from je_web_runner.utils.generate_report.report_manifest import generate_all_reports
# Run every generator + write a manifest binding all outputs:
result = generate_all_reports("run_2026_04_26", allure_dir="allure-results")
print(result["manifest_path"]) # → run_2026_04_26.manifest.json
| Format | Output shape | Spec-driven? |
|---|---|---|
| JSON | <base>_success.json + <base>_failure.json |
split |
| HTML | <base>.html |
single |
| XML | <base>_success.xml + <base>_failure.xml |
split |
| JUnit XML | <base>_junit.xml |
single |
| Allure | <allure_dir>/<uuid>-result.json (× N) |
directory |
The manifest captures the actual paths produced — CI globs no longer need to know the per-format conventions.
from je_web_runner import (
test_record_instance,
summarise_run,
notify_run_summary,
)
from je_web_runner.utils.executor.action_executor import executor
from je_web_runner.utils.observability.otel_tracing import install_executor_tracing
from je_web_runner.utils.dashboard.live_dashboard import start_dashboard
from je_web_runner.utils.replay_studio.replay_studio import export_replay_studio
executor.set_failure_screenshot_dir("./failures")
install_executor_tracing("webrunner") # one OTel span per action
start_dashboard("127.0.0.1", 8080) # browser-friendly progress UI
test_record_instance.set_record_enable(True)
# … run actions …
export_replay_studio("./run.html", screenshot_dir="./failures")
notify_run_summary("https://hooks.slack.com/services/...")
Failure screenshot, OpenTelemetry tracing, retry policy, and the live dashboard all hook into the same Executor.event_dict so they compose without coupling.
# Filter by tag, run in parallel processes, persist a ledger, fail fast on dep breaks.
python -m je_web_runner \
--execute_dir ./actions \
--tag smoke,fast \
--exclude-tag slow \
--parallel 4 \
--parallel-mode process \
--ledger ./.run_ledger.json
# Re-run only the files that failed last time:
python -m je_web_runner --execute_dir ./actions --rerun-failed ./.run_ledger.json
# Watch a directory and re-run on file change:
python -m je_web_runner --execute_dir ./actions --watch ./actions
# Distribute across 4 runners deterministically (per machine):
python -m je_web_runner --execute_dir ./actions --shard 1/4
python -m je_web_runner --execute_dir ./actions --shard 2/4
python -m je_web_runner --execute_dir ./actions --shard 3/4
python -m je_web_runner --execute_dir ./actions --shard 4/4
Companion APIs — WR_run_for_users (multi-user matrix), WR_run_ab (A/B mode), WR_flakiness_stats, WR_classify_failure, WR_schedule + WR_run_scheduler_for.
WR_lint_action / WR_lint_action_file flag legacy command names, hard-coded URLs, dangerous scripts, missing tags, duplicate consecutive actions.python -m je_web_runner --migrate ./actions rewrites the eleven legacy aliases to their preferred names (--migrate-dry-run reports without writing).WR_scan_secrets_file / WR_assert_no_secrets catch AWS / GitHub / Slack / JWT / Google / private-key strings before they land in commits.WR_audit_security_headers_url checks HSTS / CSP / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy.WR_a11y_run_audit injects user-supplied axe-core (load_axe_source) and runs against the active session; Playwright variant WR_pw_a11y_run_audit.WR_lighthouse_run shells out to the official lighthouse Node CLI; WR_lighthouse_assert_scores enforces budgets.WR_perf_collect / WR_pw_perf_collect snapshot FCP / LCP / CLS / TTFB / domContentLoaded / load via PerformanceObserver; WR_perf_assert_within checks thresholds.WR_visual_capture_baseline + WR_visual_compare (Pillow soft-dep).WR_match_snapshot / WR_update_snapshot (text/DOM, unified diff on mismatch).WR_throttle("slow_3g") / WR_pw_throttle("offline"); presets cover Slow 3G, Fast 3G, Regular 4G, Wi-Fi, Offline, no-throttling.WR_diff_har / WR_diff_har_files show added / removed / status-changed requests between two runs.executor.set_allow_arbitrary_script(False) blocks WR_execute_script / WR_execute_async_script / WR_pw_evaluate / WR_cdp / WR_pw_cdp for untrusted action JSON.Reliability & flake reduction:
je_web_runner.utils.adaptive_retry.run_with_retry(fn, policy=...) replays only failures the classifier marks transient / flaky / environment; real bugs short-circuit.linter.locator_strength.score_locator(strategy, value) ranks locators 0–100; assert_strength fails CI on fragile XPath / TAG_NAME picks.smart_wait.wait_for_fetch_idle and wait_for_spa_route_stable patch window.fetch and history.pushState to detect SPA quiescence — no more time.sleep.throttler.throttle("payments-api") is a file-semaphore that caps cross-shard concurrency on a shared service.Debugging & observability:
observability.timeline.build(spans=, console=, responses=) merges OTel spans, console messages, and network responses into one chronologically-sorted event list.failure_bundle.FailureBundle("login_test", error_repr).add_screenshot(...).write("bundle.zip") packages screenshots / DOM / network / console / trace into a single replayable zip with manifest.memory_leak.detect_growth(driver, action, iterations=10, growth_bytes_per_iter_budget=...) polls performance.memory.usedJSHeapSize and fails on linear-fit growth above budget.trace_recorder.TraceRecorder(output_dir="trace-out").start(context, name); …; .stop(context) always writes a .zip viewable with playwright show-trace.csp_reporter.CspViolationCollector injects a securitypolicyviolation listener and exposes assert_none() / assert_no_directive("script-src").Test data & determinism:
snapshot.fixture_record.FixtureRecorder("fx.json", mode="auto") saves the producer’s output the first time, replays it forever after.database.fixtures.load_fixture_file("seed.json") + load_into_connection(conn, fixture) seeds testcontainers Postgres / MySQL / SQLite from a {table: [rows]} JSON.API & contract testing:
api_mock.MockRouter().add("GET", "/api/users/*", body={"id": 1}).attach_to_page(page) intercepts Playwright routes; URL globs and re: regex patterns supported.contract_testing.validate_response(body, schema) runs a JSON-Schema subset; validate_against_openapi(body, doc, "/users/{id}", "GET", 200) resolves $ref and checks the right schema for the response status.graphql.GraphQLClient("https://api/graphql").execute("{ me { id } }"); extract_field(payload, "me.id") plucks values via dotted path.mock_services.MockOAuthServer().start() issues fake bearer tokens, MockSmtpServer captures sent mails, MockS3Storage is a memory KV.Security probes:
header_tampering.HeaderTampering().set_header("X-Forwarded-For", "192.0.2.1").attach_to_page(page) mutates outbound requests so testers can probe missing-CSRF / wrong-origin / stripped-auth handling.license_scanner.scan_text(bundle_text) finds SPDX identifiers and known license phrases (AGPL/GPL/MIT/Apache-2.0/MPL/ISC/BSD) so SBOM gates can assert_allowed_licenses.Browser & locale:
device_emulation.playwright_kwargs("iPhone 15 Pro") and apply_to_chrome_options(opts, "Desktop 1080p"); viewport + DPR + UA + touch in one call.geo_locale.GeoOverride(latitude=51.5, longitude=-0.13, timezone="Europe/London", locale="en-GB") produces both CDP commands and Playwright new_context kwargs.multi_tab.TabChoreographer().open_new(driver, "side", url=...) registers tabs by alias so action JSON can WR_switch_tab("side").webauthn.enable_virtual_authenticator(driver) uses CDP WebAuthn.* to simulate passkey / FIDO2 sign-in flows.cookie_consent.ConsentDismisser().dismiss(driver) clicks the first matching OneTrust / TrustArc / Cookiebot / Didomi / Quantcast button; selector list extensible via register_selector.Reporting & CI:
pr_comment.post_or_update_comment("owner/repo", 42, body, token=...) is idempotent via a hidden HTML marker so retried CI runs don’t pile up.trend_dashboard.compute_trend("ledger.json") buckets the ledger by day; render_html(trend) produces a self-contained SVG line chart + table.Orchestration & developer experience:
action_templates.render_template("login_basic", {...}) substitutes `` in built-in flows (login, accept-cookies, switch-locale, close-modal).sharding.diff_shard.select_for_changed(candidates, base_ref="main") filters candidates to those touched by the current branch’s git diff.watch_mode.watch_loop(directory, on_change=callback, interval=0.5) re-runs a callback whenever JSON files change.k8s_runner.render_job_manifests(ShardJobConfig(name_prefix="run", image=..., total_shards=8, actions_dir="/actions")) produces one batch/v1 Job per shard.perf_metrics.budgets.evaluate_metrics("/checkout", {"lcp_ms": 2300}, budgets) plus assert_within_budget(result) enforce route-specific thresholds.AI assistance:
ai_assist.llm_assist.explain_failure(test_name, error_repr, console=, network=, steps=) asks the registered LLM for {likely_cause, evidence, next_steps, confidence}.WebRunner ships a Model Context Protocol server so any MCP-aware client (Claude, IDE plugins, etc.) can drive WebRunner over JSON-RPC stdio.
python -m je_web_runner.mcp_server
The default tool list (22 tools) exposes:
Live browser execution:
webrunner_run_actions — execute any WR_* action list. Covers the full ~280-command surface including the advanced WebDriverWrapper additions: WR_attach_to_existing_browser, WR_execute_cdp_cmd, WR_set_timezone / _locale / _device_metrics / _user_agent / _extra_http_headers / _geolocation / _network_conditions, WR_block_urls / _set_cache_disabled / _set_download_directory, WR_save_cookies / _load_cookies / _clear_origin_storage, WR_save_full_page_screenshot / _print_page, WR_reload(ignore_cache=True), WR_bring_to_front, WR_switch_to_window_by_url|title, WR_new_window / _close_window, page metadata getters, Fetch interception primitives, WR_add_script_to_evaluate_on_new_document, …webrunner_run_action_files — batch-run JSON files on diskwebrunner_list_commands — discover the full WR_* surfacePlus the utility tools below (no live browser):
Action JSON authoring & linting:
webrunner_lint_action, webrunner_score_action_locators, webrunner_locator_strengthwebrunner_format_actions, webrunner_parse_markdown, webrunner_render_templatewebrunner_translate_actions_to_playwright, webrunner_translate_python_to_playwrightCode generation:
webrunner_pom_from_htmlQuality / triage:
webrunner_a11y_diff, webrunner_cluster_failures, webrunner_compute_trendSecurity & privacy:
webrunner_scan_pii, webrunner_redact_piiReporting & contract:
webrunner_summary_markdown, webrunner_validate_responseSharding & infra:
webrunner_diff_shard, webrunner_render_k8s, webrunner_partition_shardfrom je_web_runner.mcp_server import McpServer, Tool, build_default_tools, serve_stdio
# Or build a custom server
server = McpServer()
for tool in build_default_tools():
server.register(tool)
server.register(Tool(
name="my_custom_tool",
description="…",
input_schema={"type": "object", "properties": {"x": {"type": "string"}}},
handler=lambda args: f"hello {args['x']}",
))
serve_stdio(server=server)
The server speaks MCP 2024-11-05: initialize, tools/list, tools/call, resources/list, ping, shutdown.
A standard Language Server Protocol implementation for action JSON files:
python -m je_web_runner.action_lsp
textDocument/completion returns every registered WR_* command; textDocument/publishDiagnostics runs the action linter on didOpen / didChange. Pair with VS Code’s Configure JSON Language Servers or the JetBrains LSP plugin.
CLI & orchestration polish:
test_filter.name_filter.filter_paths(paths, include=["smoke.*"], exclude=["slow"]) keeps only matching candidate paths; orthogonal to the existing tag filter.process_supervisor.ProcessSupervisor().kill_orphans() walks the OS process table for chromedriver / geckodriver / msedgedriver and kills stragglers (skips os.getpid() automatically). with_watchdog(callable, timeout_seconds=300) wraps a long callable with a hard wall-clock raise.pipeline.load_pipeline({"stages": [...]}) + run_pipeline(pipeline, runner) execute multi-stage gates: continue_on_failure=True makes a stage non-blocking (linters / scanners), otherwise downstream stages skip.Frontend / mobile / coverage:
storybook.visual_snapshots.capture_story_snapshots(stories, base_url, take_screenshot, navigate, baseline_dir=...) walks every story, persists deterministic filenames (components-button--primary.png), and diffs against an optional baseline. assert_no_visual_regressions(report) is the gate.appium_integration.gestures ships swipe, scroll, long_press, pinch, double_tap that prefer Appium’s mobile: named-gesture extension and fall back to W3C Actions on older drivers.coverage_map.build_coverage_map("./actions") walks every action JSON file, normalises WR_to_url paths (/users/42 → /users/:id) and produces a route → files reverse index. coverage.uncovered(declared_routes) answers “which routes have no test?”.Debugging & reproducibility:
cdp_tap.CdpRecorder("cdp.ndjson").attach(driver) wraps execute_cdp_cmd so every command + return value is appended to an ndjson log; CdpReplayer(load_recording(...)) plays it back against a stub for offline debugging.cross_browser.diff_runs([chromium_run, firefox_run, webkit_run]) diffs title / DOM hash / console / network status / screenshot hash, classifying each finding as major (5xx, title, DOM mismatch) or minor. assert_parity(report, only_major=True) is the gate.state_diff.capture_state(driver) snapshots cookies + localStorage + sessionStorage; diff_states(before, after) lists added / removed / changed keys per section so cart / auth flows stay traceable.Authoring / scaffolding:
pom_codegen.discover_elements_from_html(html) walks every element with data-testid / id / form name; render_pom_module(elements, class_name="LoginPage") returns a Python module with one TestObject property per element.CI reproducibility:
workspace_lock.build_lock(drivers=..., playwright_versions={"chromium": "127.0.0.0"}) snapshots every Python distribution + driver version + Playwright browser version; write_lock(lock, ".webrunner/lock.json") and diff_locks(before, after) complete the pipeline.Long-running observability:
a11y_trend.aggregate_history(history) buckets axe runs by day and impact; render_html(points) produces a self-contained SVG line chart so regressions are visible at a glance.perf_drift.detect_drift({"lcp_ms": samples}, baseline_window=20, recent_window=5) compares the recent P95 against a rolling baseline P95 and flags drift outside tolerance. assert_no_regression(report) is the strict path; higher_is_better={"frame_rate"} for inverted metrics.Authoring / formatting:
action_formatter.format_actions(actions) writes a canonical multi-line array with kwargs in a stable preferred-then-alphabetical order; format_file(path) reformats in place and reports (text, changed).md_authoring.parse_markdown(text) understands - open <url>, - click #id, - type "x" into <selector>, - wait 3s, - assert title "...", - press Enter, - screenshot, - run template <name>, - quit. Lines that don’t match are preserved as WR__note so the round-trip is loss-less.Triage / production observability:
failure_cluster.cluster_failures(failures, top_n=5) reduces each error message to a stable signature (strips timestamps, hex addresses, line numbers, paths, large numerics, quoted substrings) so the same root cause across runs lands in one bucket.synthetic_monitoring.SyntheticMonitor(alert_sink).register("homepage", check) reruns checks; the sink only fires on edge transitions (green → red / red → green) with failure_threshold / recovery_threshold to silence flapping.observability.otlp_exporter.configure_otlp_export(provider, OtlpExportConfig(endpoint="https://otlp:4317")) ships the existing OTel spans to Jaeger / Tempo / any OTLP backend (gRPC by default, HTTP fallback).Frontend / component:
storybook.discover_stories(index_path) reads Storybook 7+ index.json (or legacy stories.json); plan_actions_for_stories(stories, base_url, run_a11y=True) builds a flat action list visiting each story in iframe mode and running axe + screenshot.dom_traversal.shadow_pierce.find_first(driver, "button.primary") recursively walks open shadow roots (Selenium execute_script or Playwright evaluate) so a single CSS selector can match across shadow boundaries.Onboarding / migration:
python -m je_web_runner --init (or bootstrapper.init_workspace("my-tests")) drops actions/sample.json, .webrunner/ledger.json, pinned-driver template, JSON schema, pre-commit hook, and a starter GitHub Actions workflow.driver_pin.install_for_browser(".webrunner/drivers.json", "firefox") reads a JSON pin file (name / version / url / archive_format / binary_inside), downloads + extracts once, then serves from cache. Bypasses the GitHub API rate limit that webdriver-manager hits in CI.sel_to_pw.translate_python_source(text) rewrites driver.find_element(By.ID, "x") → page.locator("#x") and similar; translate_action_list(actions) rewrites WR_* action JSON to its WR_pw_* equivalent (drops WR_implicitly_wait since Playwright auto-waits).Test authoring:
form_autofill.plan_fill_actions(fields, fixture, submit_locator=...) infers each field from data-testid / id / name / placeholder / label / type and emits a ready-to-run WR_save_test_object + WR_element_input sequence.Quality:
accessibility.a11y_diff.diff_violations(baseline, current) buckets axe-core findings into added / resolved / persisting keyed on (rule_id, target); assert_no_regressions(diff, allow_rules=...) is the CI gate.Performance / orchestration:
fanout.run_fan_out([("preflight-a", task_a), task_b, ...], max_workers=4) runs read-only callables concurrently inside one test, returning per-task duration + outcome with raise_for_failures() for the strict path.event_bus.EventBus(".webrunner/events.log").publish("setup-done", {"shard": 1}); subscribers poll() from a remembered offset or wait_for(topic, predicate=..., timeout=30). File-backed ndjson — no Redis dependency.Browser internals:
extension_harness.parse_manifest("./ext") reads MV2 / MV3 manifests; apply_to_chrome_options(options, [ext_dir]) adds --load-extension flags; playwright_persistent_context_args(...) returns the kwargs needed for launch_persistent_context.Reliability & dev-loop:
browser_pool.BrowserPool(factory, size=4, max_uses=50).warm(); with pool.session() as ses: … removes browser cold-start from local dev. Health check + recycle policy built in.bidi_backend.BidiBridge().subscribe(target, "console", callback) works against either Selenium 4 BiDi (driver.script.add_console_message_handler) or Playwright page.on(...). register_translator lets you wire custom event names.Determinism & offline runs:
har_replay.HarReplayServer(load_har("recorded.har")).start() boots a local HTTP server that serves recorded responses; supports literal / glob / re: URL matching with rotation across duplicates. Drop-in for staging-API outages.Quality / privacy:
pii_scanner.scan_text(text) finds emails, E.164 phones, Luhn-validated credit cards, US SSN, ROC ID, and IPv4. assert_no_pii(text, allow_categories=...) for CI gates; redact_text(text) returns a sanitised copy.visual_review.VisualReviewServer(baseline_dir, current_dir).start() opens a local web UI showing each baseline / current pair side-by-side with an Accept current as baseline button (idempotent file copy with path-traversal guard).Test orchestration:
impact_analysis.build_index("./actions") walks every action JSON file and projects locator names, URLs, template names, and WR_* commands into a reverse index; affected_action_files(index, locators=["primary_cta"]) answers “which tests touch this?” so diff-aware shards can go beyond filename matching.The Selenium wrapper is now composed via mixins under
je_web_runner/webdriver/_wrapper_mixins/ (lifecycle / element / wait stay in
webdriver_wrapper.py; cookies / actions / media / navigation / scripting are
the mixin themes). External imports — webdriver_wrapper_instance,
WebDriverWrapper, the _options_dict / _webdriver_dict /
_webdriver_manager_dict patch targets — are unchanged.
from je_web_runner import webdriver_wrapper_instance
webdriver_wrapper_instance.set_driver(
"chrome",
options=[
"--disable-blink-features=AutomationControlled",
f"--user-data-dir={profile_dir}",
],
experimental_options={
"excludeSwitches": ["enable-automation"],
"useAutomationExtension": False,
},
extension_paths=["/path/to/extension.crx"], # optional
enable_bidi=True, # for add_console_listener etc.
)
webdriver_wrapper_instance.add_script_to_evaluate_on_new_document(
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
)
# Step 1 — user starts Chrome themselves:
# chrome.exe --remote-debugging-port=9222 --user-data-dir="C:/temp/profile"
webdriver_wrapper_instance.attach_to_existing_browser("127.0.0.1:9222")
w = webdriver_wrapper_instance
w.set_timezone("Asia/Tokyo")
w.set_locale("ja-JP")
w.set_device_metrics(390, 844, device_scale_factor=3, mobile=True)
w.set_user_agent("Mozilla/5.0 (custom)")
w.set_extra_http_headers({"X-Test-Run": "ci-123"})
w.set_geolocation(35.68, 139.69, accuracy=50)
w.set_network_conditions(offline=False, latency=200,
download_throughput=50_000, upload_throughput=10_000)
w.block_urls(["*.doubleclick.net/*", "*.googletagmanager.com/*"])
w.set_cache_disabled(True)
w.set_download_directory("./downloads")
w.clear_origin_storage("https://example.com") # cookies + localStorage + IDB + cache
w.to_url("https://example.com/")
# … log in, etc. …
w.save_cookies("./cookies.json")
# Later (after browser restart):
w.to_url("https://example.com/")
added = w.load_cookies("./cookies.json") # → number of cookies applied
w.enable_fetch_interception(patterns=["*/api/*"])
# In a Fetch.requestPaused event callback (subscribe via CDPEventListener):
w.fulfill_request(req_id, response_code=200,
body=b'{"ok": true}',
response_headers={"Content-Type": "application/json"})
# Or: w.continue_request(req_id, url=rewritten_url)
# Or: w.fail_request(req_id, error_reason="AccessDenied")
w.get_current_url(); w.get_title(); w.get_page_source()
w.get_window_handles(); w.get_current_window_handle()
w.new_window("tab")
w.switch_to_window_by_url("checkout") # restores original if no match
w.close_window() # vs quit() which terminates the driver
w.reload(ignore_cache=True) # CDP Page.reload — Ctrl+Shift+R equivalent
w.bring_to_front()
w.save_full_page_screenshot("./shot.png") # full page, beyond viewport
w.print_page("./page.pdf")
webdriver_wrapper_instance.set_driver("chrome", enable_bidi=True)
sub_id = webdriver_wrapper_instance.add_console_listener(
lambda entry: print(entry.text)
)
err_id = webdriver_wrapper_instance.add_js_error_listener(
lambda err: print("page exception:", err)
)
# …later…
webdriver_wrapper_instance.remove_console_listener(sub_id)
webdriver_wrapper_instance.remove_js_error_listener(err_id)
CDPEventListener opens its own CDP WebSocket on a worker thread so commands
and events share the same target session — required because Selenium’s
execute_cdp_cmd cannot subscribe to events.
from je_web_runner import CDPEventListener
with CDPEventListener.from_driver(driver) as listener:
listener.on("Fetch.requestPaused", handle_paused)
listener.send("Fetch.enable", {"patterns": [{"urlPattern": "*"}]})
# … drive the browser …
Requires pip install websocket-client (lazy-loaded; a clear CDPEventLoopError
is raised if missing).
from je_web_runner import record_trace
record_trace(
driver, "perf.json",
categories=["devtools.timeline", "loading"],
duration=10.0,
)
# Open perf.json in chrome://tracing or DevTools "Performance".
from je_web_runner import (
bidi_add_request_handler,
bidi_add_response_handler,
bidi_clear_network_handlers,
)
sub = bidi_add_request_handler(driver, lambda req: print(req.url))
bidi_clear_network_handlers(driver)
Every method above is also reachable from action JSON via WR_* aliases
(WR_set_timezone, WR_save_cookies, WR_enable_fetch_interception, …) so
the same surface drives the MCP server too.
from je_web_runner import (
selenium_cdp, # raw CDP
pw_emulate, pw_set_locale, # mobile / locale
)
from je_web_runner.utils.storage.browser_storage import (
selenium_local_storage_set,
selenium_indexed_db_drop,
)
from je_web_runner.utils.observability.event_capture import (
start_event_capture,
assert_no_console_errors,
assert_no_5xx,
)
from je_web_runner.utils.dom_traversal.shadow_iframe import (
selenium_query_in_shadow,
playwright_shadow_selector,
selenium_switch_iframe_chain,
)
from je_web_runner.utils.file_transfer.file_helpers import (
selenium_upload_file,
wait_for_download,
)
from je_web_runner.utils.extensions.extension_loader import (
selenium_chrome_options_with_extension,
playwright_extension_launch_args,
)
Service worker / cache control, console + network event capture and assertions, file upload via element + download dir watcher, browser extension loaders for Chromium-family.
from je_web_runner import (
load_env, get_env, expand_in_action, # .env + ${ENV.X}
load_dataset_csv, load_dataset_json, run_with_dataset, # data-driven + ${ROW.x}
fake_email, fake_name, fake_credit_card, fake_value, # faker
)
from je_web_runner.utils.factories.factory import user_factory, order_factory
from je_web_runner.utils.testcontainers_integration.containers import (
start_postgres,
start_redis,
cleanup_all,
)
Every helper is JSON-callable too (WR_load_env, WR_load_dataset_csv, WR_run_with_dataset, WR_faker_email, WR_user_factory, WR_tc_postgres, …).
from je_web_runner import (
http_get, http_post, http_assert_status, http_assert_json_contains,
)
from je_web_runner.utils.auth.oauth import (
client_credentials_token,
bearer_header,
)
from je_web_runner.utils.database.db_validate import (
db_query,
db_assert_count,
db_assert_value,
)
token = client_credentials_token(
"https://idp.example/oauth2/token",
"client-id", "client-secret",
cache_key="default",
)
http_get("https://api.example/users/me", headers=bearer_header(token["access_token"]))
http_assert_status(200)
http_assert_json_contains("role", "admin")
db_assert_count(
"postgresql+psycopg://user:pw@host/db",
"SELECT 1 FROM orders WHERE user_id = :uid",
expected=1,
params={"uid": 42},
)
OAuth2 helpers cache tokens in-process and refresh 30 seconds before expiry.
from je_web_runner import (
recorder_start,
recorder_stop,
recorder_save_recording,
)
recorder_start(webdriver_wrapper_instance)
# … user clicks / inputs in the browser …
recorder_save_recording(
webdriver_wrapper_instance,
output_path="./recorded.json",
raw_events_path="./raw.json", # optional — debugging
)
The recorder injects a static JS listener (no CDP, no eval), so it works on Chrome / Firefox / Edge alike. Sensitive fields are masked by default — type=password, names matching password / card_number / cvv / ssn / secret / token / api_key / otp / passcode, and 13–19-digit numeric values are replaced with ***MASKED***.
from je_web_runner.utils.notifier.webhook_notifier import notify_run_summary
from je_web_runner.utils.test_management.jira_client import jira_create_failure_issues
from je_web_runner.utils.test_management.testrail_client import (
testrail_send_results,
testrail_results_from_pairs,
)
from je_web_runner.utils.ci_annotations.github_annotations import (
emit_failure_annotations,
emit_from_junit_xml,
)
For GitHub Actions inline annotations, run emit_from_junit_xml("run_junit.xml") after generate_junit_xml_report — failed test cases surface as ::error file=…:: lines on the PR diff.
docker/docker-compose.yml ships a Selenium Grid 4 stack (hub + Chrome + Firefox nodes); docker/.env.example exposes the version pin and concurrency settings.
The IDE config examples under docs/ide/ wire VS Code and JetBrains to the action JSON schema produced by WR_export_action_schema.
from je_web_runner.utils.ai_assist.llm_assist import (
set_llm_callable,
suggest_locator,
generate_actions_from_prompt,
)
# Plug in any callable that returns a string:
def my_llm(prompt: str) -> str:
# call OpenAI / Anthropic / local Ollama / mock
...
set_llm_callable(my_llm)
locator = suggest_locator(html_blob, description="primary submit button")
draft = generate_actions_from_prompt("log in as alice and place an order")
WebRunner intentionally ships no built-in LLM client — the boundary is a single Callable[[str], str] so swapping provider is one line.
# Original entry points (unchanged):
python -m je_web_runner -e actions.json
python -m je_web_runner -d ./actions/
python -m je_web_runner --execute_str '[["WR_quit_all"]]'
# Newer flags:
python -m je_web_runner -d ./actions --tag smoke --exclude-tag slow
python -m je_web_runner -d ./actions --parallel 4 --parallel-mode process
python -m je_web_runner -d ./actions --ledger ledger.json
python -m je_web_runner -d ./actions --rerun-failed ledger.json
python -m je_web_runner -d ./actions --shard 1/4
python -m je_web_runner -d ./actions --watch ./actions
python -m je_web_runner --report run # JSON + HTML + XML + JUnit
python -m je_web_runner --validate ./action_smoke.json
python -m je_web_runner --migrate ./actions --migrate-dry-run
Compose any of the flags above; the dispatcher applies tag filters → ledger / re-run-failed → sharding → dependency-aware ordering before handing files to the runner.
from je_web_runner import test_record_instance
test_record_instance.set_record_enable(True)
# … perform automation …
records = test_record_instance.test_record_list
# Each record: {"function_name", "local_param", "time", "program_exception"}
test_record_instance.clean_record()
WebRunner provides a hierarchy of custom exceptions — every helper raises a domain-specific subclass of WebRunnerException:
| Exception | Description |
|---|---|
WebRunnerException |
Base |
WebRunnerWebDriverNotFoundException |
WebDriver not found |
WebRunnerOptionsWrongTypeException |
Invalid options type |
WebRunnerArgumentWrongTypeException |
Invalid argument type |
WebRunnerWebDriverIsNoneException |
WebDriver is None |
WebRunnerExecuteException |
Action execution error |
WebRunnerJsonException |
JSON processing error |
WebRunnerGenerateJsonReportException |
JSON / XML / JUnit / Allure report error |
WebRunnerHTMLException |
HTML report error |
WebRunnerAddCommandException |
Custom command registration error |
WebRunnerAssertException |
Assertion failure |
XMLException / XMLTypeException |
XML processing error |
CallbackExecutorException |
Callback execution error |
PlaywrightBackendError |
Playwright backend / element failure |
PlaywrightLocatorError |
TestObject → Playwright selector mapping |
RecorderError / VisualRegressionError |
Recorder / visual regression |
HealingError / EnvConfigError / DataDrivenError |
Self-healing / env / dataset |
HttpAssertionError / HttpError |
HTTP API assertions |
AccessibilityError / LighthouseError |
a11y / Lighthouse |
NotifierError / JiraError / TestRailError |
Notifications / test management |
CDPError / StorageError / ServiceWorkerError |
Browser internals |
OAuthError / DatabaseValidationError |
Auth / DB |
NetworkEmulationError / LoadTestError |
Throttling / Locust |
ShardingError / MigrationError / ActionLinterError |
Orchestration / linting |
LLMAssistError / OTelTracingError |
AI / observability |
WebRunner uses a rotating file handler:
WEBRunner.log%(asctime)s | %(name)s | %(levelname)s | %(message)s| Browser | Selenium key | Playwright |
|---|---|---|
| Google Chrome | chrome |
chromium |
| Chromium | chromium |
chromium |
| Mozilla Firefox | firefox |
firefox |
| Microsoft Edge | edge |
chromium |
| Internet Explorer | ie |
n/a |
| Apple Safari | safari |
webkit |
This project is licensed under the MIT License.