Repository navigation
seleniumbase-rs 0.5.0
A release of API clean-ups and new structure. The fingerprint API is one
Spoof per setting with a separate StealthPolicy, built once and never
mutated. BaseCase text assertions and waits take their arguments in Python's
order, and the run settings, runner, CDP page and behave APIs are cleaned up.
The command line moved to its own workspace crate, seleniumbase-rs-cli, and
parses with usage instead of clap. The built-in plugins can be chosen by name
from features, the settings file, SB_PLUGINS or sbrs --plugin. PDF text
extraction is the opt-in feature pdf. The BREAKING changes, those that break a
call, are listed first.
Breaking
-
sbrsandsbrs-mcpare built by the new workspace package
seleniumbase-rs-cli(crates/cli), not byseleniumbase-rs. The library
stays at the repository root with the same name and path, so
seleniumbase-rs = { git = ... }keeps working, but it no longer has a binary
and no longer compiles clap,clap_completeorclap_complete_fig(15 crates
fewer in a default build). The commands, flags, aliases, defaults and exit
statuses ofsbrsare the same (its parser changed, see Changed), with one
difference: a plaincargo build,cargo installorcargo runnow includes
pdf, sosbrs pdf-textandsbrs assert-pdf-textwork where they used to exit
with status 2.# before cargo install --path . --bin sbrs cargo install --path . --bin sbrs-mcp --features mcp-server cargo install --git https://github.com/MustCodeAl/seleniumbase-rs --features mcp-server # after cargo install --path crates/cli cargo install --git https://github.com/MustCodeAl/seleniumbase-rs seleniumbase-rs-cli cargo binstall --git https://github.com/MustCodeAl/seleniumbase-rs seleniumbase-rs-cli
The new package has
pdfandmcp-serveron by default (the release archives
always had them), andplugins-builtin(the built-in browser plugins, see
Added), and forwardstui,gui,turso,test-util,
full-tracingandtokio-consoleto the library, so
cargo install ... seleniumbase-rs-cli --features tuiis the way to add the
terminal UI.default-membersnames both packages, socargo build,
cargo testandcargo run --bin sbrsat the repository root work as before;
use-p seleniumbase-rsfor the library alone. The release archives keep their
names (seleniumbase-rs-v<version>-<target>). The messagessbrsprints for a
build withouttuiorpdfnow name the package that builds it
(cargo install --git https://github.com/MustCodeAl/seleniumbase-rs seleniumbase-rs-cli --features tui), instead ofseleniumbase-rs, which no
longer has a binary. The Homebrew formula has to build--path crates/clifrom
the first release that has this change. -
PDF text extraction is the opt-in feature
pdf.pdf-extract(lopdf, fonts,
ciphers) was 34 of the crates of a default build.BaseCase::get_pdf_text,
assert_pdf_textand the moduleapi::pdfneed it; printing a page to PDF
(print_to_pdf,save_as_pdf) does not.sbrs pdf-textand
assert-pdf-textstay in--helpand, in a build without the feature, exit
with status 2 and say how to reinstall it with the feature
(cargo install --git https://github.com/MustCodeAl/seleniumbase-rs seleniumbase-rs-cli --features pdf). -
Text assertions take the text first and the selector second, as Python's
assert_text(text, selector)does. A call with the old order still compiles
(both arguments are&str), so search for these names:assert_text,
assert_exact_text,assert_exact_text_not_visible,assert_shadow_text,
RecorderSession::assert_text,AssertionApi::assert_textand the macro
sb_assert_text!. A failing assertion whose arguments look swapped says so in
its message.// before sb.assert_text("h1", "Example Domain").await?; sb_assert_text!(sb, "#status", "Saved")?; // after sb.assert_text("Example Domain", "h1").await?; sb_assert_text!(sb, "Saved", "#status")?;
The generators follow: the recorder, the scenario runner,
sbrs mkdir,
sbrs mkfile, the MCP export tools and the Python importer now write the new
order.RecorderSession::assert_text(sb, text, css)and the
AssertionApi::assert_text(expected, css)trait method changed with it. If
you generated tests with an earlier version, run the same search over them. -
The text waits take
(text, css, timeout_secs), as Python's
wait_for_text(text, selector, timeout)does.wait_for_text,
wait_for_exact_text_visibleandwait_for_exact_text_not_visibleswapped
their first two arguments (a silent change: both are&str).
wait_for_text_visible,wait_for_text_not_visibleandwait_for_exact_text
gained the selector they lacked, so an old call stops compiling. Pass"body"
to look at the whole page, as the oldwait_for_text_visible(text, timeout)
did.// before sb.wait_for_text("#status", "Saved", 10).await?; sb.wait_for_text_visible("Saved", 10).await?; sb.wait_for_exact_text("Saved", 10).await?; // after sb.wait_for_text("Saved", "#status", 10).await?; sb.wait_for_text_visible("Saved", "body", 10).await?; sb.wait_for_exact_text("Saved", "#status", 10).await?;
wait_for_textnow waits for the text to be displayed, like
wait_for_text_visible(in Python the first is an alias of the second). -
The deferred and delayed text checks take
(text, css).
deferred_assert_exact_text,delayed_assert_exact_textand
delayed_assert_texttook the selector first.delayed_assert_textalso
passed its arguments on in the other order, sodelayed_assert_text("Welcome", "h1")looked for an element calledWelcome; all three now follow Python's
(text, selector), likedeferred_assert_text.// before sb.deferred_assert_exact_text("#status", "Ready").await?; // after sb.deferred_assert_exact_text("Ready", "#status").await?;
-
find_text,find_exact_textandfind_non_empty_textfollow Python.
They took no selector, searched all of<body>for the deepest element that
held the text, and returnedResult<Option<WebElement>>(theOptionwas
neverNone). They now take the selector and returnResult<WebElement>,
the element of that selector, as Python'sfind_text(text, selector)does.
Pass"body"to look at the page.// before if let Some(el) = sb.find_text("Welcome").await? { /* ... */ } // after let el = sb.find_text("Welcome", "h1").await?;
-
post_message_and_highlight(message, css)takes the element to highlight,
as Python'spost_message_and_highlight(message, selector)does. It used to
highlight the toast it had just shown. An error while highlighting is now
returned instead of ignored.// before sb.post_message_and_highlight("Saved").await?; // after sb.post_message_and_highlight("Saved", "#save-button").await?;
-
The extra browsers are named by a
DriverId, and switching to the default
one is repeatable.get_new_driver(Option<BrowserConfig>)returned the
index of the browser it had just parked, andswitch_to_driver(index)swapped
the active browser with the one in that slot, so the slot held whichever
browser was left last:switch_to_default_driver()(slot 0) reached the
first browser only once, and its second call went back to the other. A
DriverIdnow names a browser for good, andswitch_to_default_driver()
always reaches the first one. Changes:get_new_driver()takes no argument andget_new_driver_with(config)takes
the configuration (no moreNone); both return the new browser's
DriverId.switch_to_driver(DriverId)makes that browser active and can be repeated;
active_driver()says which one is.quit_extra_driver(DriverId)refusesDriverId::DEFAULT(that browser is
closed byquit()); quitting the active extra browser falls back to the
default one, as in Python. A browser that was quit is an error to switch
to, not a disconnected session that panics.quit()closes every browser the case opened, not only the active one.driver_count()counts the browsers that are open, the active one
included (it counted the parked ones, quit placeholders too).
// before let first = sb.get_new_driver(None).await?; // the index of the parked browser sb.switch_to_driver(first).await?; // swaps: the slot now holds the other // after let second = sb.get_new_driver().await?; // the new browser's id sb.switch_to_default_driver().await?; // always the first browser sb.switch_to_driver(second).await?;
-
wait(seconds)takes whole or fractional seconds, and nou64. It is
sleep, as in Python, and takesimpl Into<f64>:wait(2)andwait(0.5)
compile, but au64variable (or ausize, ani64) no longer does.// before let seconds: u64 = 3; sb.wait(seconds).await; // after sb.wait(seconds as f64).await; // or keep the variable a u32 or an f64
-
check_window's level is au8, not anf64:check_window("home", 3)
instead ofcheck_window("home", 3.0); the same fordeferred_check_window
anddelayed_check_window. A float literal stops compiling. As before, a
level other than 0, 1, 2 or 3 is anInvalidConfigerror.// before sb.check_window("home", 3.0).await?; // after sb.check_window("home", 3).await?;
-
The cookie helpers take a name and use
saved_cookies/, as Python's do.
save_cookies(path)andload_cookies(path)took any file path, so a call
written like Python's (save_cookies("login")) wrote a file calledlogin
in the working directory. They now take a name: the file is
saved_cookies/<name>.txtin the working directory (only the last part of
the name is used,.txtis added, a name ending with/is refused), still
JSON.save_cookiesreturns the path it wrote.get_saved_cookies(name)is a
method (it was an associated function that took a path), and
delete_saved_cookies(name)deletes that file (it looked inlatest_logsand
the working directory).load_cookiesnow does what Python's does: a cookie
of another domain gets the page's, and the expiry is dropped, so the cookies
last for the browser session;load_cookies_with(name, CookieExpiry)keeps
the saved expiry or sets a new one.For a file at a path of your own there are
save_cookies_to_path(&Path)and
load_cookies_from_path(&Path, CookieExpiry); the MCP server and
sbrs save-cookies/load-cookiesuse them. The Rust-only
save_saved_cookiesandload_saved_cookiesare deprecated.// before sb.save_cookies("cookies.json").await?; // any path sb.load_cookies("cookies.json").await?; // after sb.save_cookies("login").await?; // saved_cookies/login.txt sb.load_cookies("login").await?; sb.save_cookies_to_path(Path::new("cookies.json")).await?; // any path sb.load_cookies_from_path(Path::new("cookies.json"), CookieExpiry::Saved).await?;
-
assert_data_in_downloaded_file(data, file)takes the data first, as
Python's does, andassert_data_in_downloaded_fileand
assert_downloaded_file_regexareasync: they wait up to the default timeout
for the download, as Python's do, instead of failing on a file that is still
arriving. The data is compared without the whitespace at its ends.// before sb.assert_data_in_downloaded_file("report.csv", "id,name")?; // after sb.assert_data_in_downloaded_file("id,name", "report.csv").await?;
-
sb_assert_title!andsb_assert_url!check the whole value, as
Python'sassert_titleandassert_urldo; they checked that the title or
URL contained the text. The old behaviour is in the new
sb_assert_title_contains!andsb_assert_url_contains!. A test that passed
a part of the title keeps passing only with the_containsmacro.// before sb_assert_title!(sb, "Dashboard")?; // the title contained "Dashboard" // after sb_assert_title_contains!(sb, "Dashboard")?;
-
run_browser_testandPlugins::run_browser_testtake an async closure
(F: AsyncFnOnce(&mut BaseCase) -> Result<()>) instead of a closure that
returns a boxed future. The old form|sb| Box::pin(async move { .. })does
not compile against the new bound, and neither did|sb| async move { .. }.
Migration:run_browser_test(config, |sb| Box::pin(async move { body }))
becomesrun_browser_test(config, async |sb| { body }), and
|sb| Box::pin(demo(sb, url))becomesasync |sb| demo(sb, url).await
(useasync move |sb|in a loop that moves its variables). To keep an old
closure compiling for now, callrun_browser_test_boxed
(Plugins::run_browser_test_boxed), which is deprecated like
BrowserTestFuture.sbrs mkdir,sbrs mkfileandsbrs import-python
generate the new form. -
RuntimeConfig::log_levelis anOption<String>that defaults toNone
(it was"info"), so the binaries can tell "not set" from "set to info".
Migration:config.log_levelbecomesconfig.log_level.as_deref() .unwrap_or("info")where you read it, andconfig.log_level = Some(..)
where you set it. -
Settings::load(path: Option<P>)is split in two, so nobody writes
Settings::load(None::<&str>). Migration:Settings::load(None::<&str>)?
becomesSettings::load()?, andSettings::load(Some(path))?becomes
Settings::load_from(path)?.Settings::from_tomlis deprecated; use
from_file, which reads TOML for a.tomlname. -
Settings::from_file,load,load_fromandfrom_envnow validate
(Settings::validate), so input that used to load is anInvalidConfigerror:
an unknownbrowserormode, a window width without a height (or the
reverse; the missing side used to default to 1920 by 1080), and a window
dimension of zero. Migration: spell the browser (chrome,firefox, ...) and
mode (webdriver,cdp,uc) asBrowserandDriverModeparse them, set
both window dimensions or neither, and callvalidate()yourself on a
Settingsbuilt by hand. A blankSB_*variable counts as unset (SB_PLUGINS
is the exception: an empty value turns every plugin off). -
sb_test!binds its$sbto a&mut BaseCaseinstead of an owned
BaseCase, and a skipped test passes instead of panicking. Migration: a body
that movedsb(sb.quit().await, passing it by value) now borrows it, and
the macro closes the browser. -
Settings::timeout_seconds,window_widthandwindow_heightare
Options that default toNone. The old defaults (30 s, 1920, 1080) were read by
nothing. Migration:settings.timeout_secondsissettings.timeout_seconds .unwrap_or(10)where you used the number; assignSome(..)where you set it.
screenshot_diris documented as stored for your code: the library never read
it.Settingsalso has the new public fieldswebdriver_urlandchrome_bin,
andLaunchSettingsthe newrun: a struct literal that names every field
needs..Default::default(). -
sb_cdptab and window creation no longer takes anOptionURL, which
neededNone::<&str>to open a blank tab.Browser::new_page,
Browser::new_window,BrowserContext::new_pageand the pool lease's
new_pagetake no argument and open a blank one;new_page_at(url)and
new_window_at(url)open one on a URL (a URL without a scheme is opened
over HTTPS, as before).// before let blank = browser.new_page(None::<&str>).await?; let docs = browser.new_page(Some("https://example.com")).await?; // after let blank = browser.new_page().await?; let docs = browser.new_page_at("https://example.com").await?;
-
sb_cdp::Locator::type_textempties the field before it types, as Python's
sb.cdp.typedoes ("clears the text field first"); it used to type after
what the field held, which is Python'ssend_keysand the new
Locator::send_keys. This changes behaviour without changing the signature,
so a call that meant to append must be renamed. The MCPtype_texttool keeps
itsfill_inputandappendmodes.CdpDriver::type_textnow sends key
events too (it inserted the text in one step), like the othertype_text
methods.// before: appended page.locator("#note").type_text(" more").await?; // after page.locator("#note").send_keys(" more").await?;
-
behave::AsyncStepRegistryisSend + Sync: a run can be handed to
tokio::spawnand the registry shared in anArc. Its handlers get the
BaseCaseasSharedBaseCase(Arc<tokio::sync::Mutex<BaseCase>>) instead
ofRc<RefCell<BaseCase>>, return anAsyncStepFuture(Send) and must be
Send + Sync;with_baseisasync. New:base_case()and
into_base_case(), which hands theBaseCaseback after a run so the caller
canquitit (no step needs to).// before registry.register_regex(r#"^I click "(.+?)"$"#, Box::new(|bc, _step, caps| { let selector = caps[0].clone(); Box::pin(async move { bc.borrow_mut().click(&selector).await.map_err(|e| e.to_string()) }) })); // after: lock the shared BaseCase registry.register_regex(r#"^I click "(.+?)"$"#, Box::new(|bc, _step, caps| { let selector = caps[0].clone(); Box::pin(async move { bc.lock().await.click(&selector).await.map_err(|e| e.to_string()) }) })); // before: registry.with_base(|sb| ...); after: registry.with_base(|sb| ...).await
-
CdpDriver::evaluatereturns the value, as Python'ssb.cdp.evaluatedoes,
and drops areturnthat starts the last line; it used to return the raw
CDP result object.CdpDriver::findreports a selector the page rejects as
InvalidSelector(it wasCdpDriver).// before let title = driver.evaluate("document.title").await?["value"].clone(); // after let title = driver.evaluate("document.title").await?;
-
api::cdp_page::CdpPageis stricter and truer to Python'ssb.cdp(and
theBaseCase::cdp_*methods on it):evaluate(BaseCase::cdp_evaluate) returns the value instead of the raw
CDP result object{"type": ..., "value": ...}, drops a finalreturn, and
a script that throws isCdpDriver(it wasUnsupported).click(cdp_click) waits up to 7 seconds for the element, scrolls it
into view, and fails when the browser refuses an input event; it used to
discard those errors and click an element below the fold off screen.
click_with_timeouttakes another wait.get_text,get_attributeandget_propertyfail withElementNotFound
for an element that is not there;get_textused to answer""and the
othersNone. An element without the attribute still givesNone.select_option_by_valuefails withInvalidSelectorfor a value the
dropdown has no option for (it set the value and reported success), and
all threeselect_option_by_*fail withElementNotFoundfor no dropdown.- Only
clickwaits for its element; the reads,type_text,fast_typeand
the selects look once (wait_for_element_presentfirst for a late
element). A selector the browser rejects isInvalidSelectorin all of
them, andwait_for_element_presentreturns a lost connection at once
instead of polling it until the timeout and calling itElementNotFound.
// before: the value was inside the result object let title = page.evaluate("document.title").await?["value"].clone(); // after let title = page.evaluate("document.title").await?;
-
click_with_offsetanddouble_click_with_offsettakecenterand count
from the top-left corner of the element, as Python's do (click_with_offset( selector, x, y, center=False)). They passed(x, y)straight to WebDriver,
which counts from the centre of the element, while their documentation said
"from the top-left corner", so every click was off by half the element's
size. The extra argument stops old calls compiling; to keep the old behaviour
(and the click it really made) passtrue, and to get what the
documentation promised passfalse. They also wait for the element to be
displayed now. The recorder writes the new form
(sb.click_with_offset("#a", 10, -4, false)), and a recording of an offset
from the centre saysx, y, center.// before: x and y counted from the centre, whatever the docs said sb.click_with_offset("#canvas", 10, -4).await?; // after sb.click_with_offset("#canvas", 10, -4, true).await?; // from the centre, as before sb.click_with_offset("#canvas", 10, 4, false).await?; // from the top-left corner
-
save_qa_reportwrites the active QA session. It took the steps and the
title again (save_qa_report(&steps, title, path)) and ignored the session
the test had built, so the caller cloned the steps out and passed the title a
second time.save_qa_report()writes the session's report to the logs
folder,save_qa_report_to(path)to a file of your choice, and both are an
InvalidConfigerror when there is no session.qa_report_markdownis
deprecated forMasterQaSession::to_markdown.// before let steps = sb.qa_session().map(|s| s.steps.clone()).unwrap_or_default(); let report = sb.save_qa_report(&steps, "Login", None::<&str>)?; let there = sb.save_qa_report(&steps, "Login", Some("qa.md"))?; // after let report = sb.save_qa_report()?; let there = sb.save_qa_report_to("qa.md")?;
-
BaseCase::set_messenger_themetakes Python's arguments, a
MessengerSettings(theme, location, how many messages), or a
MessengerThemeorMessengerLocationalone, asPage::set_messenger_theme
does, instead of a CSS colour. It loads Messenger first. The colour setter it
was is nowset_messenger_color. A call with a colour string stops compiling.// before sb.set_messenger_theme("#336699").await?; // after sb.set_messenger_color("#336699").await?; // the same thing sb.set_messenger_theme(MessengerTheme::Air).await?; // Python's set_messenger_theme
-
post_message_for(message, duration)takes aDuration, not a number of
whole seconds, so a message can last 2.55 s as Python's default does. It also
waits for the message to go now (see Changed).// before sb.post_message_for("Saved", 2).await?; // after sb.post_message_for("Saved", Duration::from_secs(2)).await?;
-
A setting is one
Spoof<T>.Naturalis the browser's own value (the
default),Randomis a valid value drawn when the fingerprint is built and
coherent with the rest (same operating system, region and locale; repeatable
with.seed(u64)),Value(T)is exactly this (a plain value converts to it),
andOffturns the API off, for geolocation, WebRTC, media devices, battery,
Bluetooth, speech voices, network information and Client Hints only.
MaskingMode,NoiseMode,PopupMode,ProxyMaskingMode,StartupBehavior
andCanvasNoiseModeare removed (CanvasNoisehasNatural,Persistent,
RandomandLow). -
FingerprintBuilder::build()returnsResult<Fingerprint, FingerprintError>.
It fails, naming the setting, whenOffis given to a setting with no off
state, whenRandomis given to one with nothing to draw (proxy,oscpu,
build_id), when a proxy has no host, and when nothing coherent can be drawn
(a random locale next to a time zone the table of places lacks). -
A built
Fingerprinthas private fields and read accessors
(fp.screen(),fp.user_agent(), ...). To change one, takefp.to_builder(),
change it and build again.FingerprintBuildersetters take
impl Into<Spoof<T>>;geolocation,webgl,webrtc,avail_screenand
media_devicestake a tuple, andscreen_spoofis theSpoofform of
screen(width, height). The value types areScreenSize,Geolocation,
WebGl,WebRtcAddressesandMediaDevices. -
StealthFlagsisStealthPolicy(StealthFlagsstays as a deprecated
alias). A policy is how the claims are delivered, and it never changes a
setting:webrtc_policy,canvas_noise,audio_noise,native_spoofing,
native_tostring,chrome_runtime,headless_fixes,grant_permissions,
block_trackers,blocked_trackers,blocked_ports,quic,disable_csp
andopen_shadow_roots. Presets aredefault(),balanced(),
native_only()andmax(). Set it with.policy(..),.with_policy(|p| ..)
and the shortcuts.webrtc_policy,.canvas_noise,.audio_noise,
.native_spoofing,.grant_permissions,.block_trackers,
.blocked_trackers,.blocked_ports,.disable_cspand.quic, in any
order. -
Removed:
FingerprintBuilder::{flags, with_flags, canvas_seed, audio_seed, webrtc_ips, webgl_ids, altitude, accuracy, media_device_specs}and the
deprecatedmedia_devices(a, b, c)(the newmedia_devicestakes a
Spoof<MediaDevices>;media_device_counts(a, b, c)stays); the public fields
ofFingerprint;Fingerprint::{fills_defaults, geolocation_popup, seed_value}.os_cpuis nowoscpu.Fingerprint::seed()is au64. -
The built-in presets set their battery, network information, speech voices of
the system and Web Bluetooth, and Safari has none of the three it lacks (Off).
Fingerprint::randomizedis the builder with the settings of a persona
Random; its noise seed is the seed it was given. -
A fingerprint is serialised with
"natural"settings left out,"random"and
"off"as words, and the value otherwise. A fingerprint saved by an older
version (the flatscreen_width, theflagsobject) no longer loads: it is an
error that names the field, not a silent loss. A battery time that is infinite
is written asnull. -
A fingerprint with a
Randomsetting that has no.seed(..)takes fresh
entropy, and a fingerprint with nothing to draw takes a seed from its user
agent, so a preset is the same every time. -
Profile payloads read one set of words for every key (see the table in
docs/tutorials/fingerprint_stealth.md):natural,native,pass_through,
whitelistareNatural;auto, and a flag the payload leaves out, are the
value the payload holds, elseNatural(the default wasmaskfor the
navigator, localization, time zone, graphics, fonts, screen, geolocation and
WebRTC flags, which filled in generic values nobody asked for);mask,
spoof,custom,generic,random,noiseare the held value, else a
drawn one;off,disabled,block,emptyturn geolocation, WebRTC and
media devices off (they used to leave them natural) and are read asNatural
elsewhere; an unknown word is logged and read asNatural(it used to
switch the dimension on and fill it with generic values). A literal is taken
only in the format of its key (a locale orAccept-Languagelist, an IANA
zone, a comma-separated list of fonts, aMozilla/user agent,vendor~renderer);
any other text is an unknown word. An empty string in a section is a value the
payload does not give, a navigator section without a core count no longer
invents 8, and the payload'score_versionis the Chromium major a drawn user
agent claims. A profile's folder and name seed its draws.
geolocation_popupis the state of the geolocation setting:blockdenies,
promptasks.to_browser_config,user_agent(),locale(),proxy_string()
andapply_runtime_overridesread the built fingerprint, sonaturaland the
off words launch with no--user-agent, locale, proxy, window size or position. -
The Linux preset reports an ANGLE-over-OpenGL renderer instead of Direct3D,
and the macOS preset reports ANGLE over Metal. The presets leave the position
Natural, because a position is answered without a prompt and a fresh browser
asks. The iOS preset claims nonavigator.deviceMemoryand no Client Hints
(Safari has neither).validate()warns about a time zone without a position
only when the policy grants permissions. -
fingerprint!(edge)is the new Edge preset, not the Windows Chrome one. -
profile_payloads::{Flags, Fingerprint, ProxyConfig}are renamed
ProfileFlags,ProfileFingerprintandProfileProxy(the old names are
deprecated aliases). -
The MCP tool
build_stealth_bootstrapis removed: it returned the same script
asget_stealth_bootstrap_script. That script, and the providers
list_evasion_providerslists, are now the 27 a browser session runs
(session_registry), not the 29 ofdefault_registry.build_fingerprint
returns the new JSON shape (screen: {width, height}, noflags). -
Fingerprint::default()equalsFingerprint::builder().build(), and JSON
without anos_typetakes it from the user agent. AValue(true)or
Value(false)for Bluetooth now changesgetAvailability()on a browser that
has Web Bluetooth. -
The companion repository seleniumbase-rs-stealth
takes this crate from git and still uses the old API (field writes such as
fp.timezone = ..,fp.flags.*). Itsscrapeandmimiccrates must move to
to_builder()...build()?before it follows this change; onlycodecis known
to build against it. -
evasions::launch_argspasses--no-sandboxand--disable-setuid-sandbox
only on Linux, asStealthOptionsdoes.
Migrating a fingerprint:
// before: the intent is spread over a value, a flag and a mutation after build
let mut fp = Fingerprint::builder()
.screen(1920, 1080)
.geolocation(40.0, -74.0)
.flags(StealthFlags::balanced())
.build();
fp.flags.screen_masking = MaskingMode::Custom;
fp.flags.native_spoofing = true;
let width = fp.screen_width; // Option<u32>
// after: each intent is given once, and a built fingerprint is read, not edited
let fp = Fingerprint::builder()
.screen(1920, 1080)
.geolocation((40.0, -74.0)) // or Spoof::Random, or Spoof::Off to deny
.policy(StealthPolicy::balanced())
.native_spoofing(true)
.build()?;
let width = fp.screen().value().map(|s| s.width);
// to change one thing later
let wider = fp.to_builder().screen(2560, 1440).build()?;| Before | After |
|---|---|
fp.flags.x_masking = MaskingMode::Natural |
leave the setting out, or give Spoof::Natural |
MaskingMode::Mask with no value (a generic fill) |
Spoof::Random (a coherent drawn value) |
PopupMode::Block |
.geolocation(Spoof::Off) |
.webrtc_ips(public, local) |
.webrtc((public, local)) |
.webgl(vendor, renderer) and .webgl_ids(..) |
.webgl((vendor, renderer)), or WebGl::new(..).ids(..) |
.geolocation(lat, lon).altitude(a).accuracy(m) |
.geolocation(Geolocation::new(lat, lon).altitude(a).accuracy(m)) |
.flags(StealthFlags::balanced()) |
.policy(StealthPolicy::balanced()) |
.with_flags(|f| f.grant_permissions = true) |
.grant_permissions(true) or .with_policy(|p| p.grant_permissions(true)) |
.canvas_seed(v), .seed(v) |
.seed(v) and .canvas_noise(CanvasNoise::Persistent) |
fp.user_agent.as_deref() |
fp.user_agent().as_str() |
fp.is_masked(Dimension::Screen) |
unchanged |
Added
-
sb_assert_visible!, the macroassert_visible!was missing thesb_
prefix of. -
PlaywrightSession::connect_over_cdp(endpoint)attaches to a running
Chromium (for examplesb_cdp::Browser::http_url()) and works on the page it
has open;closedetaches and leaves the browser running. Python reaches
Playwright the same way (sb.cdp.get_endpoint_url()). -
run_cdp_test(options, test)andPlugins::run_cdp_test(name, options, test)run a test on the first tab of a Pure CDP browser and always close it,
the wayrun_browser_testdoes for WebDriver. -
RuntimeConfig::from_env_lenient(the config and the list of variables that
did not parse),from_varsandfrom_vars_lenient(read the variables from
a function instead of the process environment). -
BrowserConfig::with_run_settings(RunSettings)(andrun_settings()) sets
the demo, slow, message and timeout settings a test starts with, as Python's
SB(demo=, slow=, demo_sleep=, message_duration=)does. They stay out of
configuration files and of the key that decides which tests share a browser.
RunSettings::with_default_timeoutis new andSettings::timeout_seconds
feeds it. ABaseCasefromBaseCase::new(so the test runners and
sb_test!too) starts with them and with that default timeout. -
BrowserConfig::with_remote_webdriver_timeout(Duration)sets
remote_webdriver_timeout_seconds, which had no builder; a part of a second
rounds up. The commonwith_*builders ofBrowserConfiggained doc
comments. -
Settings::load_from(path)andSettings::validate;DriverModeimplements
FromStrandDisplay(webdriver,cdp,uc) and hasname(). -
RunSettings::pause_for(PauseKind)names whatpause(tiny: bool)took as a
flag:PauseKind::Fullafter an action that changed the page,Short(1/3.4
of the time in demo mode) after one that did not.message_duration()keeps
the milliseconds of--message-duration(defaultDEFAULT_MESSAGE_DURATION,
Python's 2.55 s) andmessage_duration_headless()cuts it to 0.75 s as
Python does in a headless browser. New gettersslow_mode()and
demo_sleep().pauseandmessage_secondsare deprecated now that
BaseCaseuses the new ones. -
BaseCase::demo_pauses()(featuretest-util) lists thePauseKindof each
pause demo and slow mode have made, so a test can assert that an action
paused without timing it. -
BaseCase::click_if_visible_with_timeout(selector, Duration)waits up to the
time given for the element to be displayed before it gives up without an
error, as Python'sclick_if_visible(selector, timeout=...)does;
click_if_visible(selector)still looks once, like Python'stimeout=0. -
BaseCase::cdp_insert_text(text):Input.insertTextinto whatever has focus,
the honest name ofcdp_type_text, which types nothing (it inserts, with no
key events). -
RunSettings,PauseKind,SettingsandSettingsOverridesare re-exported
at the crate root, besideRuntimeConfig. -
sb_cdp::Page::origin,locale_code,screen_rect(aScreenRectof the
display) andclick_active_element: Python'ssb.cdp.get_origin,
get_locale_code,get_screen_rectandclick_active_element. -
sb_cdp::Locator::click_with_offset(x, y, center)(the offset from the
top-left corner, or from the centre) andclick_if_visible()(one look, like
Python's defaulttimeout=0;click_if_visible_with_timeout(timeout)waits):
Python'ssb.cdp.click_with_offsetandclick_if_visible. -
behave::StepRegistry::register_stepandAsyncStepRegistry::register_step
(andtry_register_step) register a handler for the step text alone, which
matches whatever keyword the step is written with, or none, like behave's
@step;behave::step_pattern(the regex they build) is public. The
built-in async browser steps also understand the wording of Python
SeleniumBase's--rec-behaverecorder:Open "url",Click "selector",
Type text "text" into "selector",Go back,Go forward, in single or
double quotes, so a recording from Python runs here. -
sb_cdp::Page::set_messenger_themetakesimpl Into<MessengerSettings>, and
MessengerSettingsisFrom<MessengerTheme>andFrom<MessengerLocation>:
page.set_messenger_theme(MessengerTheme::Air)changes one thing and keeps
Python's defaults for the rest. PassingMessengerSettingsstill works. -
The built-in plugins, one cargo feature each (ADR 005, core side of
phase 1):plugin-shield,plugin-block-resourcesandplugin-crx, each of
which turns onpluginsand compiles only its own plugin crate, and
plugins-builtinfor all three. They come from the crate
seleniumbase-rs-plugins-builtin, re-exported asseleniumbase_rs::builtin_plugins. -
Choosing plugins by name, with no code. A
[plugins]table in the
settings file (enable = [...], and one options table per plugin) and the
SB_PLUGINSvariable (a comma-separated list, which replacesenable; an
empty value turns every plugin off) become a strictPluginSetthrough the
newplugin_hostmodule.LaunchOptionsBuilder::plugins_namedand
plugin_selection(errors atbuild()), andBrowserConfig::with_plugins_named
andwith_plugin_selection, take the names. An unknown name is an error that
lists the known ones and the cargo features that add the others; options for
a plugin that is not enabled, options a plugin refuses and a name enabled
twice are errors too. A build without thepluginsfeature refuses a
non-empty selection when it loads the settings (Settings::plugins).sbrs
andsbrs-mcpapply the table and the variable, and take command-line
options (next entry). Seedocs/plugins/selecting.md. -
Plugins on the command line (ADR 005, phase 1).
sbrsandsbrs-mcphave
the built-in plugins compiled in:plugins-builtinis a default feature of
seleniumbase-rs-cli(withpdfandmcp-server, so the release archives and
cargo installhave them), andplugins,plugin-shield,
plugin-block-resourcesandplugin-crxpass through to the library. They
honour the[plugins]table of the settings file andSB_PLUGINSfor every
command that starts a browser in either engine (BaseCase, andsbrs devtools
on the pure CDP engine), whichsbrsignored before, and add two global
options that go before the command and repeat:
--plugin NAME, and--plugin-opt NAME.KEY=VALUEwith a TOML value, as in
sbrs --plugin shield --plugin-opt 'shield.level="strict"' open URL. The
precedence is the command line overSB_PLUGINSover the file (said in
--help):--pluginadds a name after the ones the others turn on,
--plugin-optreplaces one key and keeps the plugin's other options. New
commandssbrs plugin list(name, category, whether it is on, summary) and
sbrs plugin info NAME(category, phase, capabilities with their risk and
whether the host enforces them, what it needs, ordering, the options in effect
and, if the plugin refuses them or cannot start without any, why); an unknown
name exits with status 2 and lists the known ones.sbrs testpasses the
names on to the tests asSB_PLUGINSand refuses--plugin-opt, which the
variable cannot carry.sbrs-mcptakes--pluginand--plugin-opttoo, and
reads the table and the variable from the folder it runs in; the operator
chooses the plugins, and no tool argument adds one. A mistake (an unknown
name, an option of a plugin nothing turns on, an option the plugin refuses, a
value that is not TOML such asshield.level=strict) is reported with exit
status 2 before any browser starts or the MCP server listens. A build without
plugins(--no-default-features) fails closed with the feature to build
with. The Docker image is built without the default features and has no
plugins. Only the[plugins]table of the settings file is read bysbrs devtools,sbrs plugin list|infoandsbrs-mcp, so a value of the file that
is wrong for a browser does not stop them; a command that starts no browser
ignores the options, even a wrong one. A--pluginthat turns a plugin back
on afterSB_PLUGINSleft it out keeps the options the file gave it. Every
active plugin is logged atINFOwith the place that turned it on, and a
plugin that declareslaunch.proxy,artifacts.writeor ahost.*
capability is refused when only thesbase_config.tomlof the current folder
names it: a cloned project must not grant it (name it with--pluginor
SB_PLUGINS, or use--config). The library side:
PluginSelection::apply_flags,enable_once,prune_unused,
set_enabled_keeping_optionsandto_list(andPluginSelectionisEq),
Settings::load_plugin_selectionandload_plugin_table(the plugin part of
the settings only), the modulecli::plugin_cmd(list,info,
check_selection,PluginChoice,PluginSource,apply_to_config),
DevtoolsRequest::plugins,mcp::Settings::with_plugin_selectionand
mcp::serve_with. Seedocs/plugins/selecting.md. -
The shield plugin is documented (
docs/plugins/shield.md): what it turns
off, its levels, WebRTC modes and options, and what has not been verified in a
real Chrome. -
Browser::extension_ids()keeps the ids Chrome gave the extensions a launch
loaded (Extensions.loadUnpacked), in load order. -
Spoof<T>withis_natural,is_random,is_value,is_off,is_set,
value,as_strandmap, and the policy typesStealthPolicy,
CanvasNoiseandAudioNoise. -
Fingerprint::{to_builder, is_masked, validate, randomized, randomized_with_major};Dimensionlists what can be masked and
Fingerprint::is_masked(Dimension)is the one question the page-script
providers, the launch arguments, the protocol overrides and the proxy ask. -
FingerprintBuilder::{seed, core_version, avail_screen, product_sub, build_id, app_version, media_device_counts}and the policy shortcuts.
build()derives the language list from the locale and the OS persona and
navigator.platformfrom a recognised user agent, only when unset. -
Fingerprint::edge_desktop()(Edg/user agent,Microsoft EdgeClient
Hints throughrandomize::{edge_user_agent, edge_hints}), also in the MCP
preset list;randomize::locale_of_timezone. -
evasions::session_script, the script the pure CDP engine runs, which the
WebDriverUcandCdpmodes now run too. -
An
OffAPI is removed from the page, as a browser without it would be:
getBattery,navigator.connection,navigator.bluetooth,
navigator.userAgentData, WebRTC;Offmedia devices and speech voices list
nothing; a denied geolocation answersPERMISSION_DENIED. -
CanvasNoise::{Random, Persistent, Low}andAudioNoise::Randomadd noise. -
Fingerprint::validate()reports a WebGL renderer from another OS's graphics
API and aMobileSafaripersona with aChrome/user agent as errors, and a
locale from another country than the time zone as a warning.
Deprecated
-
assert_visible!: usesb_assert_visible!. -
save_saved_cookiesandload_saved_cookies: usesave_cookiesand
load_cookies. -
PlaywrightSession::launch_headless,launch_with_fingerprintand
launch_headless_with_fingerprint: they are shortcuts for
launch_with_config(&BrowserConfig::default().with_headless(..) .with_fingerprint(..)). -
run_settings::DEFAULT_MESSAGE_SECONDS(3): useDEFAULT_MESSAGE_DURATION
(2.55 s).RunSettings::message_secondsnow roundsmessage_duration()up,
so the two cannot drift apart; the default is still 3. It is deprecated too,
withRunSettings::pause(tiny): usemessage_duration()and
pause_for(PauseKind). -
sb_cdp::Locator::click_at(Point): useclick_with_offset(x, y, false),
Python's name for it.click_atcalls it. -
BaseCase::cdp_type_text: renamedcdp_insert_text. -
BaseCase::qa_report_markdown: useMasterQaSession::to_markdown. -
BaseCase::cdp_click_element: usecdp_click, which it now calls (see
Fixed).sbrs cdp-click-elementandcdp-type-textkeep their names. -
evasions::cdp_overrides: usesafe_cdp_overrides, which leaves out the
Proxy-Authorizationheader that would hand the proxy's login to every site. -
StealthFlags(useStealthPolicy),StealthPolicy::{mask_all, all_custom}
(usemax).
Changed
-
sbrsandsbrs-mcpparse their command line with usage
instead of clap.clap,clap_complete,clap_complete_figandclap_usage
leave the workspace. The commands, flags, aliases, defaults and exit statuses are
those of 0.4.0: both builds were run on 256 command lines, 250 act the same and the
six that do not fall into the three groups below.completions/sbrs.usage.kdlis
now written from the Rust declaration by a test that fails when the file is stale
(SB_UPDATE_COMPLETIONS=1 cargo test -p seleniumbase-rs-cli --bin sbrs usage_spec
writes it again), and so are the new pagedocs/help/cli_reference.md(a CLI
reference rendered from the spec) and the Fig specdocs/completions/fig-sbrs.ts.
What you can notice:sbrs --helpis laid out by usage: the sections areCommands:andFlags:, the
commands are listed alphabetically instead of in the order they were declared, a
short description keeps the final period of its doc comment, an alias is shown as
[aliases: new], and the usage line readsUsage: sbrs [FLAGS] <SUBCOMMAND>. The
banner still comes first.sbrs completions bash|zsh|fish|elvish|powershellprint a short script that asks
sbrsfor the candidates when the completion key is pressed (sosbrshas to be
onPATH), instead of a script that lists every command. A path argument
completes file names, as before.completions figis written by
crates/cli/src/fig.rsfrom the tables of the parser: it marks an option as
required or repeatable only when it is, where theclap_complete_figoutput marked
every option that takes a value as repeatable.completions usageprints the spec.- Three groups of command lines behave differently, because usage is looser than
clap in these places. A flag repeated after a subcommand
(sbrs wait-for-text --css a --css b) takes its last value instead of ending
with status 2; the options before the
subcommand and those ofgrid-hub,grid-nodeandpromptare still refused,
since usage can set that rule on the root and on a struct, not on the inline
variants of the command enum. A value written after a switch,--headless=false
or--headed=true, is accepted and means what it says (the parser would have
ignored the value and switched the option on); any other value, such as
--headless=0, is still an error.sbrs help nosuchprints the main help and
exits with 0, where it ended with status 2.
-
ActionRecorder::to_gherkinwrites Python SeleniumBase's--rec-behave
wording,Open "url",Click "selector",Type "text" into "selector",
Go backandGo forward, where it wroteI open "url"and the like, so a
recorded feature runs in either implementation. An argument that holds a
double quote is written in single quotes, as Python does (it used to be
skipped); one with both kinds of quote is still skipped. A feature written by
an earlier version keeps running: theI opensteps are still registered.
The built-inTypestep accepts what Python's recorder really writes,
Type "text" into "selector"(it understood onlyType text ...). -
Demo mode and slow mode follow Python's rule for how long to pause, and
reach the calls Python's do.openalways paused in full,clickalways
in full and typing always short; Python pauses after an action in full when
the page's URL changed and for 1/3.4 of the time when it did not (slow mode:
always in full).BaseCasenow reads the URL before and after such an
action, in demo mode only, and pauses byRunSettings::pause_for. The pauses
and the outline now also coverdouble_click,context_click, the link
clicks,click_active_element,choose_file, the selects,add_text,
send_keys,set_value,submit,focus,go_back,go_forward, the JS
and jQuery clicks,drag_and_drop, the offset clicks and the window size and
position calls;press_keyspauses in full after a typed Enter. A demo or
slow run is longer for it: leave them off unless a person watches. -
post_message,post_message_for,post_success_messageand
post_error_messagelast the run's message duration to the millisecond
(2.55 s by default, not 3 s), at most 0.75 s in a headless browser, and wait
for the message to go and 0.15 s more, as Python's defaultpause=True
does; they returned at once. They share one box:post_message_forno longer
draws its own.post_message_and_highlightstill does not wait. -
check_windowbaselines can be told apart by test. The baseline folder
was named after the thread's last path part, so tests of the same function
name in different modules or files shared one folder, and a body run by
#[tokio::main]ortokio::spawnwas filed undermainor
tokio-runtime-worker.BaseCase::with_test_name("module::test")(builder,
withtest_name()to read it back) names the test, and the baselines go in
visual_baseline/module__test/<name>.sb_test!sets it from the module path
and the function, so its tests get a new folder: the first run saves a
baseline there again. Without a test name nothing changes. -
The element assertions and waits mean what Python's mean.
assert_element,
assert_element_visible,assert_elementsandwait_for_elementneed the
element to be displayed and wait for it (wait_for_elementfor its own
timeout, the others for the default one); they used to look once for an
element in the DOM, hidden or not. A test that asserted a hidden element (a hidden input, a
display: nonetemplate) withassert_elementmust use
assert_element_present, which now waits as well, like
assert_elements_present.wait_for_query_selectoris
wait_for_element_present, as in Python, andwait_for_element_present
takes&self.AssertionApi::assert_elementis visible-and-waiting for the
pure CDPPagetoo. -
sb_test!counts a skipped test as passed. A body that ends with
skip_testused to panic withtest failed, and now logs the reason and
passes, likerun_browser_test(libtest cannot report a skip). A test that
relied on the panic must fail with an error instead. -
load_cookieswaits for the page to finish loading first, as Python's does,
and the domain it gives a foreign cookie is the page's host without its port
(a browser driver refuses a cookie domain with a port). -
assert_shadow_text(text, selector), the text waits and the deferred checks
changed their argument order silently (both are&str). Onlyassert_text
andassert_exact_textsay "arguments look swapped" when they fail; search
your tests for the other names listed above. -
add_textwaits for the element to be displayed (it used to fail on an
element that was not there yet),send_keysisadd_text, andupdate_text
istype_text, as in Python, where they are the same methods. -
waitissleep, as in Python (the two differed:waittook whole seconds
and skipped whatsleepdoes); see the breaking change above for its new
argument. -
The getters and assertions that only read the page take
&selfinstead of
&mut self:get_title,get_current_url,get_text,assert_title,
assert_title_contains,assert_url,assert_url_contains,assert_text,
assert_exact_text,assert_text_visible,assert_text_not_visible,
assert_exact_text_not_visibleandassert_element_visible, so a test can
call them from a closure that holds only&BaseCase. Existing calls compile
as before. -
The text assertions wait, as Python's do.
assert_text,
assert_exact_text,assert_text_visible,assert_text_not_visibleand
assert_exact_text_not_visiblepoll for up to the default timeout
(set_timeout) instead of reading the page once, so a page that is still
rendering no longer fails them. The first three also require the element to
be displayed, andassert_exact_textandis_exact_text_visibleignore
whitespace at the ends of both strings. A failing assertion now arrives after
the timeout rather than at once: lower it withset_timeoutwhere a test
expects a failure.assert_text_visibleisassert_text, as in Python. The
deferred checks wait 2 seconds, whatever the default is. -
convert_xpath_to_csshandles more XPath: paths (//a/bbecomesa > b,
//a//bbecomesa b,/html/body/...), a position before the other tests
(tag[n]becomestag:nth-of-type(n),*[n]becomes*:nth-child(n)),
contains(@a,'v'),starts-with(@a,'v')(an addition; Python has none),
id('x')andor(a comma list). A form with no exact CSS equivalent (a
position after another test,text(),parent::) is still refused instead
of being converted into something close. -
The Python importer passes
By.XPATHlocators through as XPath, so XPath
that CSS cannot say is no longer refused. -
The Python importer matches the arguments of a call with Python's parameters:
self.assert_text("x")gets Python's default selectorbody, and keyword
arguments (selector=,text=,timeout=) work. It imports
assert_text_visible,assert_text_not_visible,
assert_exact_text_not_visible,wait_for_text,wait_for_exact_text, the
deferred and delayed asserts,process_deferred_assertsand
enter_mfa_code, which it used to leave as TODOs. An argument Rust cannot
carry over (timeout=on an assertion, abythat is not CSS) is an error
instead of being dropped.send_keysbecomesadd_text, nottype_text:
Python's and Selenium'ssend_keysdo not clear the field. The timeout of
wait_for_element_visibleandwait_for_element_presentis read from
timeout=; a second positional argument is Python'sby, not a timeout. -
Both dependencies on the plugins repository are pinned to one revision
(628991a):seleniumbase-rs-pluginandseleniumbase-rs-plugins-builtin
must name the same source, or Cargo builds two plugin API crates with
unrelatedPlugintraits.tests/plugin_features.rsfails when they differ.
A plugin crate added withcargo add --gitnow needs the same--rev; the
plugins guide says so. -
Plugin settings reach the browser.
LaunchSettings::host_resolver_rules
becomes one--host-resolver-rulesswitch,disable_featuresand
enable_featuresone--disable-featuresand one--enable-featuresswitch
(the first merged with theTranslatethe library turns off),webrtc_policy
--force-webrtc-ip-handling-policy, anddisable_quic--disable-quic, on
the pure CDP engine and through chromedriver. They were dropped before. The
profile is shown to plugins before they run (persistent whenuser_data_dir
is set), soshield'sephemeral_profilecan refuse a persistent one. A
plugin that sets one of these fields for Firefox or Safari is refused. -
Plugins are checked against the engine that hosts them
(PluginSet::check_with): the pure CDP engine supports every capability,
WebDriver with Chromium, Firefox or Safari the ones its protocol allows, and a
WebDriver server that answersbrowserName: tauriis re-checked as an
embedded webview once the session exists. A plugin that declares no metadata
keeps running on Firefox and Safari as before. -
A plugin's raw
Fetch.*command is refused, on the browser and on a tab,
in both engines: the host owns request interception, and the library's own
proxy login uses it. A plugin that blocks URLs sendsNetwork.setBlockedURLs. -
SeleniumBaseError::Pluginnames the plugin for theUnsupported,
Capability,Undeclared,ConflictandSealederrors of the plugin API,
and its hint explains them.
Fixed
-
BaseCase::get_locale_codemade upen-USwhen the browser gave no
language; Python returns whatnavigator.language || navigator.languages[0]
answers, and so does this now (the empty string for no locale). -
BaseCase::cdp_click_elementpressed at WebDriver's rectangle of the
element, which is in the document, but the DevTools mouse event takes
viewport coordinates, so on a page scrolled by N pixels the click landed N
pixels too low (or on another element). It is nowcdp_click: it scrolls the
element into view and presses at its centre in the viewport. The
documentation ofcdp_mouse_clicksaid "screen coordinates"; they are
viewport coordinates. Behaviour change: likecdp_click, it now takes a CSS
selector only (it went through WebDriver and also took XPath,link=and
the like), and a missing element is waited for before it fails. Useclick
for the other selector forms. A recording writes the click ascdp_click,
so generated programs do not call the deprecated method. -
BaseCase::drag_and_dropoutlines the drop selector too in demo mode, as
Python does; it outlined only the drag selector. -
BaseCase::click_with_offsetanddouble_click_with_offsetscroll the
element into view before they measure it, as Python does, so an offset on an
element below the fold lands in the right place. -
BrowserConfig::with_run_settingsdocuments that it replaces the whole
RunSettings, the default wait ofSettings::to_browser_configincluded. -
RunSettingsdemo and slow mode pause for 0.5 s after an action, as Python
4.55.2 does (DEFAULT_DEMO_MODE_TIMEOUT);DEFAULT_DEMO_PAUSEwas 1.2 s.
Passwith_demo_sleepto keep a longer pause. -
RunSettings::with_fast_mode(--fast) no longer cancels demo and slow
mode. Python reads--fastonly in CDP mode, where it skips short waits this
crate never makes, so the method is deprecated and does nothing. Behaviour
change: a run with demo or slow mode and--fastnow pauses; drop the
with_fast_mode(true)call to keep the old effect of "no pauses" by turning
the mode off instead. -
Settings::to_browser_confighonoursSB_WEBDRIVER_URLandSB_CHROME_BIN
(the new fieldsSettings::webdriver_urlandchrome_bin). Before, they
reached onlyBrowserConfig::from_envandsbrs, which patched them in by
hand, so thesbrs mkdirtest helper ignored them.SB_WINDOW_WIDTHand
SB_WINDOW_HEIGHTnow size the browser window (when both are set). -
An unknown browser or driver mode is an error.
SB_BROWSER=chromor
mode = "stealth"used to give Chrome in WebDriver mode without a word;
Settings::from_file,load,load_fromandfrom_envnow call the new
Settings::validate, which also refuses a window width without a height. -
sbrsandsbrs-mcpfall back toRUST_LOGwhenSB_LOG_LEVELis unset,
blank or invalid (then toinfo);RUST_LOGused to be ignored, and the
docs said both. One badSB_*runtime variable no longer drops the good
ones: both binaries now useRuntimeConfig::from_env_lenient, so a typo in
SB_SHUTDOWN_TIMEOUT_SECSkeepsSB_LOG_LEVEL=debugand is logged as a
warning after logging starts. -
A test that panics no longer leaves its browser open.
run_browser_test,
Plugins::run_browser_testandsb_test!now share one lifecycle that
catches the panic, closes the browser, tells the plugins the test failed and
then raises the panic again.sb_test!used to panic on a skipped test and
to hide the test's error whenquitalso failed; it now behaves like
run_browser_test. -
PlaywrightSession(featureplaywright) sizes the window from
BrowserConfig::with_window_sizeinstead of always starting maximised at
1920 by 1080. Without a configured size it behaves as before. -
BaseCase::activate_playwright_mode(featureplaywright) starts its
Chromium as the case'sBrowserConfigsays (headless or not, proxy, user
agent, locale, fingerprint, binary path, window size), where it always
started a default headed one and ignored the configuration; the profile
folder is left out, since the WebDriver browser holds it.quit()now closes
the Playwright session too; it was left running. Behaviour change: a headless
configuration now gives a headless Playwright browser, and a test that called
session.close()itself beforequit()should leave that toquit(). -
sb_cdp::Locatorreads (text,html,attribute,info,bounding_box,
viewport_rect) reported every failure asElementNotFound. Only a timeout
does now. An invalid CSS or XPath selector isInvalidSelector(for reads
and for actions, which used to sayCdpDriver), and a lost connection keeps
its own error. -
The pure CDP programs
ActionRecorder::to_cdp_scriptandto_cdp_testwrite
followLocator::type_textnow emptying the field (see Breaking): a recorded
add_textorsend_keysis.send_keys(..), which adds to the text, where
.type_text(..)would have erased it;type_textandupdate_textare one
.type_text(..)call, not aclearand atype_text; and a
click_with_offsetis.click_with_offset(x, y, false)(truefor a
recording that saysx, y, center), not the deprecatedclick_at. A
program recorded with an earlier version keeps working but clears the field
before anadd_text: record it again, or rename that call tosend_keys.
A breaking change to the fingerprint API: one field per setting, and intent
given once. Before, every setting was kept twice, as a value and as a mode that
said whether to use it (fp.screen_width and fp.flags.screen_masking), which
allowed states that mean nothing, and callers mutated the fingerprint after
build() to say what they meant. Now every setting is one Spoof<T>
(Natural, Random, Value, Off), the behaviour that is not a setting is a
StealthPolicy, and a built Fingerprint is never mutated. The Python-parity
methods (parity/api.toml) are unchanged; the fingerprint API is Rust-only. The
design is docs/design/fingerprint-api-v2.md.
enter_totp_codeandenter_mfa_codenow press Enter after the code, as
Python'senter_mfa_codedoes (it types the code and a newline). A test that
clicked a submit button after them submits twice and can drop the click.wait_for_exact_textlooked at every element of the page,<title>,
<script>and hidden elements included, so it could find text that no
visible element showed. It now looks at the selected element and requires it
to be displayed.sb_assert_not_visible!without a message did not compile, andsb_wait_and_click!
without a message dropped the error of the wait.sb_test!quit the browser
before it reported the test's result, so a quit that failed hid the test's own
error; it now reports both.- Pooled browsers shared the state of a plugin.
PluginSet::for_launch()
now runs before every launch, attach and WebDriver session, so a plugin that
offersper_launch(the CRX plugin's temporary folders, shield's origin list)
gets a fresh instance for each browser, andon_closereaches the instance
that started. - A plugin's
Network.setBlockedURLsreplaced the blocklist of the tab.
Raw sends from several plugins, and the library's own ad-block list, are now
merged into one list for each tab (each window with chromedriver). The
tracker hosts of a fingerprint withblock_trackersare part of that list on
both engines, and with chromedriver a window keeps its union when the session
switches away and back withad_blockon (switching re-sent the library's
list alone, which replaced the plugins' patterns). - A switch in
extra_argsreplaced the one plugins built. On the pure CDP
engine--disable-features,--enable-featuresand--host-resolver-rules
given inextra_argsare now joined into the one switch (as the WebDriver
engine already did), and the WebRTC policy of the options wins over a
--force-webrtc-ip-handling-policyargument; before, Chrome kept the last
one and dropped the merged list, shield's included. SB_PLUGINS=did not turn plugins off when the file had an options table
such as[plugins.shield](the build failed with stray options), and in a
build without thepluginsfeature the file's[plugins]table stopped the
load beforeSB_PLUGINScould override it. The variable now drops the
options of the plugins it no longer names and is applied before the check
for plugin support.- A plugin's
Network.setRequestInterceptionis refused likeFetch.*.