Moving an existing Python SeleniumBase or Selenium WebDriver test suite to Rust
is rarely a pure translation exercise. Tests often contain dynamic expressions,
custom helpers, fixtures, and framework plugins that cannot be converted
automatically. The sbrs import-python command is designed to handle the
mechanical parts and surface the parts that need human review.
This page explains how to use the importer, what it can and cannot translate, and how to validate the generated Rust.
When to use the importer#
Use sbrs import-python when:
- You have many simple, linear tests that mostly navigate, click, type, and assert.
- You want a starting point for a manual migration.
- You need to compare the original Python with generated Rust side by side.
Do not expect the importer to produce production-ready Rust for complex suites without review. Treat its output as a first draft.
Convert a file from the CLI#
The tool reads the Python file, detects whether it looks like SeleniumBase or
plain Selenium WebDriver, and writes a Rust test that uses #[tokio::test] and
run_browser_test.
Diagnostics are printed to standard error. If you omit --output, the generated
Rust is written to standard output so you can inspect it before saving. Unless
you pass --test-name, the test function is named after the Python file
(login_test here). The file is written even when there are problems, but the
command then exits with an error if any diagnostic has the Error severity.
The importer skips import, from, def, class, decorator and with SB(
lines and reads the statements that remain, so every statement in the file ends
up in one generated test, even when the Python file has several test methods.
Split the test methods by hand: every statement of the file lands in one generated test.
Override detection#
The importer tries to detect the source API automatically, but you can override it:
Force the source API interpretation.
Name the generated test function. Any valid Rust identifier.
Write generated Rust to a file instead of stdout.
What the importer converts#
The importer reads one statement at a time and understands these. In all cases
the arguments must be string (or, for waits and sleep, number) literals.
Arguments are matched with Python’s parameters, so selector="#a" and
timeout=5 work as well as positional ones, and a left-out selector is body,
as in Python.
SeleniumBase, as self. or sb. calls:
- Navigation:
open,visit,get,refresh,refresh_page,go_back,go_forward,maximize_window. - Interactions:
click,click_link,click_link_text,click_partial_link_text,double_click,right_click,type,type_text,update_text(which clear the field),add_textandsend_keys(which add to it, as in Python),enter_mfa_code,clear,submit,hover,check_if_unchecked,uncheck_if_checked,select_option_by_text. - Assertions:
assert_element,assert_element_visible,assert_text,assert_text_visible,assert_exact_text,assert_text_not_visible,assert_exact_text_not_visible,assert_title,assert_title_contains,assert_url,assert_url_contains. The text assertions take the text first and the selector second in Rust too, so the arguments are copied as they are. Atimeoutargument is refused (the assertion waits for the default timeout), and so is abythat is not the CSS strategy. - Soft assertions:
deferred_assert_element,deferred_assert_text,deferred_assert_exact_text(and theirdelayed_names) andprocess_deferred_asserts. - Reads:
get_text,get_attribute(the value is bound to an unused variable). - Waits:
wait_for_element_visible,wait_for_element_present,wait_for_textandwait_for_exact_text(and their_visiblenames). The timeout is thetimeout=argument and defaults to 10 seconds. Andsleep.
Selenium WebDriver, as driver. or self.driver. calls:
- Navigation:
driver.get(url),maximize_window(),back(),forward(),refresh().driver.quit()anddriver.close()are dropped, because the generated test closes the browser itself. - Saved element variables assigned from
find_elementcalls, anddriver.find_element(...)used directly, followed byclick,clear,submitorsend_keyswith a string literal (send_keysbecomesadd_text, since Selenium’s does not clear the field);.textand.get_attribute()on CSS-based variables. - Locators:
By.CSS_SELECTOR,By.ID,By.NAME,By.CLASS_NAME(one class),By.TAG_NAME,By.XPATH,By.LINK_TEXT,By.PARTIAL_LINK_TEXT. Link-text locators only supportclick. - Simple XPath expressions that can be safely approximated as CSS selectors.
- Title checks:
assert "text" in driver.titleandassert driver.title == "text".
What requires manual review#
The importer is conservative. Anything it does not understand is emitted as a
TODO comment or a diagnostic. Common cases that need manual work include:
- Dynamic expressions and string formatting for selectors or URLs.
- Control flow such as loops, conditionals, and try/except blocks.
- Complex XPath that has no CSS equivalent.
- Custom helpers and page-object classes.
- Pytest fixtures, parametrization, and plugins.
- File uploads, downloads, and non-standard API calls.
- Selenium
WebDriverWait,Select(...).select_by_visible_text,ActionChainsand keyboard chords forsend_keys.
A statement the importer does not recognise becomes a TODO comment and a
Warning diagnostic. One it recognises but cannot translate safely (a
non-literal argument, an XPath with no CSS equivalent, an unknown By strategy)
becomes a TODO comment and an Error diagnostic, which makes the CLI exit
with an error.
Always review the generated file before running it.
Example conversion#
Given this Python file, the importer produces the Rust file next to it (the layout is rustfmt-able but not formatted):
Validate the generated Rust#
After conversion, run these steps before trusting the test:
Resolve the TODOs
Read the file and resolve every TODO comment.
Format
Run cargo fmt to normalize formatting.
Compile
Compile with cargo check or cargo build.
Run it safely
Run the test against a safe environment first.
Review the diagnostics
Review diagnostics printed by the importer.
Programmatic API#
The same importer is available from Rust code through import_python,
ImportOptions, and PythonSource:
result.detected_source says which API family the importer assumed, and
result.is_complete() is true when no diagnostic has the Error severity.
Migration strategy#
For large suites, a full automated migration is usually not practical. A more realistic approach is:
Start small
Pick a small, stable subset of tests to migrate first.
Generate drafts
Use sbrs import-python to generate drafts.
Refactor
Refactor the generated code into helpers and page objects in Rust.
Run both suites
Run both Python and Rust suites in parallel until confidence is high.
Migrate the rest
Gradually migrate the remaining tests, prioritizing the most valuable ones.