Generated from CHANGELOG.md by tools/docs7_changelog.py. The versions follow
Semantic Versioning.
Latest release: v0.5.1#
The releases publish no prebuilt binaries; install the command line from the tag:
All releases#
Downloads: Release notes · Source (.zip) · Source (.tar.gz)
A patch release of the command line: it refuses again what clap refused in 0.4.0,
and sbrs-mcp names itself.
Changed#
sbrs-mcpnames itself:sbrs-mcp --versionprintssbrs-mcp 0.5.1, and its help and the usage line of an error saysbrs-mcp, where they saidseleniumbase-rs.
Fixed#
- A flag given twice after a subcommand,
sbrs wait-for-text --css a --css b, is an error with status 2 again (the argument '--css' cannot be used multiple times), as it was with clap. 0.5.0 took the last value. A flag that is a list, such asdeferred --assert, still repeats. - A value written after a switch,
sbrs --headless=false open ...or--help=1, is an error with status 2 again, as it was with clap. 0.5.0 acceptedtrueandfalsethere and ignored them. sbrs help nosuch, andhelpwith any path that names no command, is an error with status 2 and anunrecognized subcommandmessage again. 0.5.0 printed the help of the last command it found, with status 0.
Downloads: Release notes · Source (.zip) · Source (.tar.gz)
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 packageseleniumbase-rs-cli(crates/cli), not byseleniumbase-rs. The library stays at the repository root with the same name and path, soseleniumbase-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 includespdf, sosbrs pdf-textandsbrs assert-pdf-textwork where they used to exit with status 2.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, socargo 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-textandassert-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 macrosb_assert_text!. A failing assertion whose arguments look swapped says so in its message.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 theAssertionApi::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’swait_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_textgained 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.wait_for_textnow waits for the text to be displayed, likewait_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_textanddelayed_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. -
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. -
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. -
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. ADriverIdnow 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’sDriverId.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).
-
wait(seconds)takes whole or fractional seconds, and nou64. It issleep, as in Python, and takesimpl Into<f64>:wait(2)andwait(0.5)compile, but au64variable (or ausize, ani64) no longer does. -
check_window’s level is au8, not anf64:check_window("home", 3)instead ofcheck_window("home", 3.0); the same fordeferred_check_windowanddelayed_check_window. A float literal stops compiling. As before, a level other than 0, 1, 2 or 3 is anInvalidConfigerror. -
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 calledloginin the working directory. They now take a name: the file issaved_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), anddelete_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)andload_cookies_from_path(&Path, CookieExpiry); the MCP server andsbrs save-cookies/load-cookiesuse them. The Rust-onlysave_saved_cookiesandload_saved_cookiesare deprecated. -
assert_data_in_downloaded_file(data, file)takes the data first, as Python’s does, andassert_data_in_downloaded_fileandassert_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. -
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 newsb_assert_title_contains!andsb_assert_url_contains!. A test that passed a part of the title keeps passing only with the_containsmacro. -
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 likeBrowserTestFuture.sbrs mkdir,sbrs mkfileandsbrs import-pythongenerate 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 writesSettings::load(None::<&str>). Migration:Settings::load(None::<&str>)?becomesSettings::load()?, andSettings::load(Some(path))?becomesSettings::load_from(path)?.Settings::from_tomlis deprecated; usefrom_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 aSettingsbuilt by hand. A blankSB_*variable counts as unset (SB_PLUGINSis the exception: an empty value turns every plugin off). -
sb_test!binds its$sbto a&mut BaseCaseinstead of an ownedBaseCase, 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_heightareOptions 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’snew_pagetake no argument and open a blank one;new_page_at(url)andnew_window_at(url)open one on a URL (a URL without a scheme is opened over HTTPS, as before). -
sb_cdp::Locator::type_textempties the field before it types, as Python’ssb.cdp.typedoes (“clears the text field first”); it used to type after what the field held, which is Python’ssend_keysand the newLocator::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_textmethods. -
behave::AsyncStepRegistryisSend + Sync: a run can be handed totokio::spawnand the registry shared in anArc. Its handlers get theBaseCaseasSharedBaseCase(Arc<tokio::sync::Mutex<BaseCase>>) instead ofRc<RefCell<BaseCase>>, return anAsyncStepFuture(Send) and must beSend + Sync;with_baseisasync. New:base_case()andinto_base_case(), which hands theBaseCaseback after a run so the caller canquitit (no step needs to). -
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 asInvalidSelector(it wasCdpDriver). -
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 withElementNotFoundfor 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.
-
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. -
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 anInvalidConfigerror when there is no session.qa_report_markdownis deprecated forMasterQaSession::to_markdown. -
BaseCase::set_messenger_themetakes Python’s arguments, aMessengerSettings(theme, location, how many messages), or aMessengerThemeorMessengerLocationalone, asPage::set_messenger_themedoes, 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. -
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). -
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,StartupBehaviorandCanvasNoiseModeare 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 takeimpl Into<Spoof<T>>;geolocation,webgl,webrtc,avail_screenandmedia_devicestake a tuple, andscreen_spoofis theSpoofform ofscreen(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_cspandopen_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 aSpoof<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 personaRandom; 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 asNaturalelsewhere; 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 renamedProfileFlags,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 providerslist_evasion_providerslists, are now the 27 a browser session runs (session_registry), not the 29 ofdefault_registry.build_fingerprintreturns 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)orValue(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 toto_builder()...build()?before it follows this change; onlycodecis known to build against it. -
evasions::launch_argspasses--no-sandboxand--disable-setuid-sandboxonly on Linux, asStealthOptionsdoes.
Migrating a fingerprint:
| 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’sSB(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_secondsfeeds it. ABaseCasefromBaseCase::new(so the test runners andsb_test!too) starts with them and with that default timeout. -
BrowserConfig::with_remote_webdriver_timeout(Duration)setsremote_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;DriverModeimplementsFromStrandDisplay(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()anddemo_sleep().pauseandmessage_secondsare deprecated now thatBaseCaseuses 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>, andMessengerSettingsisFrom<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, andplugins-builtinfor all three. They come from the crateseleniumbase-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 theSB_PLUGINSvariable (a comma-separated list, which replacesenable; an empty value turns every plugin off) become a strictPluginSetthrough the newplugin_hostmodule.LaunchOptionsBuilder::plugins_namedandplugin_selection(errors atbuild()), andBrowserConfig::with_plugins_namedandwith_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).sbrsandsbrs-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 ofseleniumbase-rs-cli(withpdfandmcp-server, so the release archives andcargo 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 devtoolson 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 insbrs --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) andsbrs 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 withoutplugins(--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--pluginorSB_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_selectionandmcp::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 andFingerprint::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 andnavigator.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_cookiesandload_cookies. -
PlaywrightSession::launch_headless,launch_with_fingerprintandlaunch_headless_with_fingerprint: they are shortcuts forlaunch_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()andpause_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 theProxy-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_usageleave 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_specwrites 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 askssbrsfor 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 bycrates/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=falseor--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-behavewording,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_messageandpost_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=Truedoes; 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 undermainortokio-runtime-worker.BaseCase::with_test_name("module::test")(builder, withtest_name()to read it back) names the test, and the baselines go invisual_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, adisplay: nonetemplate) withassert_elementmust useassert_element_present, which now waits as well, likeassert_elements_present.wait_for_query_selectoriswait_for_element_present, as in Python, andwait_for_element_presenttakes&self.AssertionApi::assert_elementis visible-and-waiting for the pure CDPPagetoo. -
sb_test!counts a skipped test as passed. A body that ends withskip_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_textandassert_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_textistype_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_visibleandassert_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 importsassert_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_assertsandenter_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 ofwait_for_element_visibleandwait_for_element_presentis read fromtimeout=; 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-builtinmust 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_rulesbecomes one--host-resolver-rulesswitch,disable_featuresandenable_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_diris 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. Useclickfor 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 wholeRunSettings, 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 thewith_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_WIDTHandSB_WINDOW_HEIGHTnow size the browser window (when both are set). -
An unknown browser or driver mode is an error.
SB_BROWSER=chromormode = "stealth"used to give Chrome in WebDriver mode without a word;Settings::from_file,load,load_fromandfrom_envnow call the newSettings::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 inSB_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 likerun_browser_test. -
PlaywrightSession(featureplaywright) sizes the window fromBrowserConfig::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 calledsession.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 recordedadd_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 aclick_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-rulesgiven 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.*.
Downloads: Release notes · Source (.zip) · Source (.tar.gz)
A feature release: browser plugins now run in the WebDriver engine as well as
the Pure CDP one, the Pure CDP engine gains page script and style tags and HAR
recording, the opt-in visual feature compares screenshots by structural
similarity, sbrs prompt prints a browser and driver segment for shell prompts,
and the documentation gains a guide to testing pages with a CAPTCHA.
Added#
- Browser plugins for the WebDriver engine (feature
plugins; ADR 004 phase 3).BrowserConfig::with_pluginsgives aPluginSettoBaseCase::newandBrowserSession::connect: the set is checked against the browser,before_launchchanges the start-up settings (Chromium arguments, capability preferences, extension folders, proxy and headless; Firefox arguments, preferences and headless), and once the session runs the plugins geton_browser, then the packages andon_page_createdfor the first window, for each windowswitch_to_new_windoworopen_new_windowopens, and for any other onapply_plugins_to_window;quitor a drop callson_close, andreconnectruns the hooks again. On Chromium browsers plugins send protocol commands through chromedriver’s DevTools channel; on Firefox and Safarisendis refused and scripts for every document run at once and again after eachopen. A refusal or a failing hook stops the session withSeleniumBaseError::Plugin. Plugins withreuse_sessionare refused. See the WebDriver section ofdocs/tutorials/plugins.md. sb_cdp::Page::add_script_tagandadd_style_tagadd a script or a stylesheet to the current document from a URL or inline (TagSource), waiting for a file to load;add_init_scriptruns a script in every document the tab loads from then on and returns anInitScriptIdforremove_init_script. Playwright’saddScriptTag,addStyleTagandaddInitScript.- With the
pluginsfeature,sb_cdp::Pageimplements the plugin API’sPageHandle, so a program can hand one of its tabs to plugin code. - Visual regression testing, behind the new opt-in
visualfeature:visual::VisualBaselinesaves a screenshot as a baseline PNG on the first run and compares later screenshots with it by structural similarity (MSSIM over 8 × 8 windows on each colour channel,image-compare), failing below a threshold of 0.999 withAssertionFailedand leaving<name>.actual.pngand a<name>.diff.pngthat paints the differences red. Screenshots of another size fail without a comparison.check_pagetakes aBaseCaseor ansb_cdp::Page, andBaseCase::assert_visual_match(name)keeps its baselines invisual_baseline/<test>/,<test>being the test’s whole path (test binary, module path and function, ascart.checkout.renders), and followswith_visual_baseline_reset.check_windowis unchanged: it compares the HTML layout, as in Python. - HAR recording for the pure CDP engine (no new dependency, no feature):
Page::start_har()/start_har_with(HarOptions)return aHarRecorder, andHarRecorder::finish()returns aHar(HTTP Archive 1.2 serde types) thatHar::save(path)writes as JSON. Entries come from theNetworkevents of the tab and of its cross-site iframes, which run in targets of their own and are attached while recording, extra-info headers included, with cookies, query strings, POST bodies, one entry per redirect hop, failures with Chrome’s_error, andblocked/dns/connect/ssl/send/wait/receivetimings; each top-level navigation is a page withonContentLoadandonLoad. Response bodies are off by default and capped at 1 MiB each when turned on; the cap can be raised to 64 MiB (MAX_CONTENT_BYTES). sbrs promptprints one short line for a shell prompt: the installed Chrome’s major version and whether a matching chromedriver is at hand (chrome 155 ✓driver,✗driver,?driver), with--asciimarks and a--formatof{browser},{major},{driver},{mark}and{driver_major}. It starts no browser and uses no network: versions come from files (a macOS app’sInfo.plist, the version folder besidechrome.exe), and one that only--versiontells is cached for ten minutes in the user cache folder. It prints nothing and exits with 0 when no browser is found (--verbosesays why). The CLI page has setups for Bash, Zsh, Fish, PowerShell, Starship and Warp, and.warp/workflows/a Workflow for it.browser::driver_version::major_without_runningreads a program’s major version from its files only, andversion_flag_majorruns--versionwith a time limit.- Tutorial Testing Pages with a CAPTCHA: vendor test keys, and tests that click the test widget and read the token.
docs/completions/: a Fig completion spec forsbrs, prepared for withfig/autocomplete (not submitted), with the steps.
Fixed#
- The pure CDP engine takes DevTools messages up to 256 MiB, as Puppeteer does. Chrome sends each answer in one WebSocket frame, and one over the default 16 MiB, such as a large screenshot or response body, used to close the connection for every tab.
sbrsexits quietly with status 0 when the reader of its output goes away (sbrs completions zsh | head), instead of printing “error: Broken pipe”.- Canvas noise ran only for one mode;
Persistent,RandomandLowswitched it off. - WebDriver sessions in the
UcandCdpmodes ran the 29-provider script of a browser withoutnavigator.plugins(replaced getters a real Chrome does not have), while the pure CDP engine ran the 27-provider one. - An allowed geolocation popup did not grant the geolocation permission, so the
position override waited on a prompt nobody presses. The ports a fingerprint
lists (
blocked_ports) were stored and never blocked; they are now refused onlocalhost,127.0.0.1and[::1]through the sameNetwork.setBlockedURLscommand as the tracker list. The Modernizr hairline fix followedgraphics_noiseand was switched off byLow,MediumandHigh; it follows the headless fixes. - Ad blocking in the
CdpandUcmodes replaced the fingerprint’s tracker and port block, because a secondNetwork.setBlockedURLsreplaces the first; the command now carries both. --user-agent,--lang,--window-sizeand--proxy-serverwere sent twice, from the options and from the fingerprint; each is sent once, with the value that was already in effect.- The
Accept-Languageof the user agent override was sent (with an inventeden-US,en;q=0.9) whenever a user agent was set; it is sent only when the localization is on. - The Tauri profile manager example applies a fingerprint with
Page::apply_fingerprintinstead of its own copy, which used the unsafecdp_overridesand the 29-provider script. Its custom masking form reads and writes the new fingerprint JSON (a screen is onescreenobject, the selects say what to do with the settings left empty), and profiles saved in the old flat shape no longer load.
Downloads: Release notes · Source (.zip) · Source (.tar.gz)
A breaking release: the experimental stealth scraping layer leaves this crate.
Removed#
- BREAKING: the stealth scraping layer moved to its own repository,
seleniumbase-rs-stealth.
The features
scrapeandimpersonate, the modulesseleniumbase_rs::wire,seleniumbase_rs::scrapeandseleniumbase_rs::impersonate, and their crate-root re-exports (BrowserScraper,CookieJar,StealthIdentity,SessionPolicy,ScrapeError,BrowserKind,Emulation,TlsSpec,H2Spec,HeaderSpec,ImpersonationProfile,CustomProfileBuilder,impersonation_client,HttpSession,MitmProxy,StealthSession,EphemeralCaand the rest) are gone from this crate. They are now the cratesseleniumbase-rs-wire(was the modulewire),seleniumbase-rs-scrape(was the featurescrape) andseleniumbase-rs-impersonate(was the featureimpersonate), which build on the public API of this crate and stay experimental. Depend on them by git, for exampleseleniumbase-rs-scrape = { git = "https://github.com/MustCodeAl/seleniumbase-rs-stealth" }, and giveseleniumbase-rsby its plain git URL next to them so both resolve to one commit (Stealth Scraping). - With the layer go the optional dependencies
psl,wreq,btls,tokio-btls,brotli,hyper,hyper-utilandhttp-body-util; the examplescapture_tls,capture_hints,capture_h2,cookie_jar,browser_scraper,custom_profile,http_sessionandstealth_session; the tests and fixtures of the layer; thejust scrape-gate,imp-gateandmeasurerecipes and their mise tasks; thescrapeCI matrix entries and theimpersonateCI job; and CMake, NASM and LLVM from the build requirements. futures-utilwithout its default macros (select!/join!, unused), so thefutures-macroproc macro is no longer compiled.scraperwithout its default features (getoptsfor its own CLI, unused parse-error text).flate2asks only for thezlib-rsbackendzipalready builds (pdf-extractstill turns on its default one).- The unused
tracing-futuresdependency (every build compiled it) and the unusedtracing-actixone from thefull-tracingfeature. - JSON logs come from
tracing-subscriber’s own JSON format instead of the separatejson-subscribercrate. - The
playwrightfeature no longer listsrustwright-coreitself;rustwrightalready brings it without Python. - HTTPS uses rustls with the
ringcrypto provider for every client, so theaws-lc-sysC library is no longer compiled. A program that builds its own reqwest client next to this crate callsseleniumbase_rs::install_tls_provider()first. - reqwest no longer builds its
blockingclient: ending reused sessions at exit uses the async client on a small runtime of its own. thirtyfouris built without itscomponentderive macros and its own drivermanager(this crate downloads drivers itself), which also removes its olderzipanddirs.zipis built with deflate only (the format of driver downloads and the bundled extensions) instead of every format; zstd (a C library), bzip2, lzma, xz, ppmd and AES no longer compile.
Fixed#
- A WebDriver URL with a path and no trailing slash (a Grid’s
http://hub:4444/wd/hub, or tauri-plugin-sbrs-automation’shttp://127.0.0.1:4445/<token>) lost its last path segment, because the client joins command paths as relative URLs; the slash is added when connecting.
Changed#
-
The generated browser identity claims Chrome 154, 155 or 156 (was 153 to 155), and its high-entropy Client Hints name a real build per major (
stealth::randomize::KNOWN_FULL_VERSIONS: 155.0.8059.40 measured on macOS 27.0.1 bytests/client_hints_chrome.rs, 154.0.8037.100 and 156.0.8078.35 from Google’s version history) instead ofmajor.0.0.0. Macs claim macOS 15.8.1, 26.7.1, 27.0.0 or 27.0.1; iPhones may claim Safari 26.0 with the frozeniPhone OS 18_6(Safari 17.7 is gone). The Chrome presets, the--mobileuser agent and the examples claim Chrome 156 (PRESET_CHROMIUM_MAJOR), the Android ones in Chrome’s frozenLinux; Android 10; Kform, and the iPhone preset is Safari 26.0. Seeds map to other identities than before. -
A fingerprint without Client Hints (the presets) gets its brand list, decoy brand and full versions from
chromium_hints(major)instead of a fixedNot(A:Brand24, the Client Hints platform name (Windows, notWin32), and afullVersionList. -
The
sbrsbanner reads SeleniumBaseRust (figlet small font, 68 columns), andsbrs --helpandsbrs-mcp --helpsay SeleniumBaseRust. -
The tests’ leftover-process checks (Edge, geckodriver, the MCP stdio servers) list processes with
sysinfo(a dev-dependency) instead ofpgrepandkill -0, so they run on Windows too instead of being skipped there. -
Human-paced input draws its bell-curve values (
stealth::humanize::Rng::gaussian, used bystealth::behavior) fromrand_distr’sStandardNormalinstead of a hand-written Box-Muller transform. The distribution is the same, but a seededHumanizernow plans different paths and timings than before for the same seed.humanize::Rngimplementsrand::TryRng(sorand::Rng), andrand_distris a new dependency.
Added#
-
sbrs devtoolslaunches Chrome with its DevTools endpoint open (--port, default 9222), prints the endpoint and configs for Google’schrome-devtools-mcpand Microsoft’s Playwright MCP (--jsonfor one line of JSON), and runs until Ctrl-C, which closes the browser. The Pure CDP MCP server hasget_devtools_endpoint, andBrowser::devtools_endpoint()andLaunchOptionsBuilder::debugging_portdo the same from Rust. Seedocs/tutorials/devtools_mcp.md, which also covers Firefox’s and Safari’s MCP servers. -
.github/workflows/release.ymlbuildssbrsandsbrs-mcpfor five targets on a version tag and attaches the archives, with completions and Warp workflows, to the release;[package.metadata.binstall]pointscargo binstallat them. Not run yet (GitHub Actions is locked on the account). -
Feature
ocr(off by default):ocr::Ocrreads the text a page shows from its screenshot (read_page, for aBaseCaseor ansb_cdp::Page) or from PNG bytes (text_in_png), with ocrs, pure Rust. Its two models (about 12 MB) are downloaded into the cache folder on first use and checked against pinned SHA-256 hashes;SB_OCR_MODELSnames a folder of your own copies instead. -
Browser plugins for the Pure CDP engine (feature
plugins, off by default; ADR 004 phase 2).LaunchOptionsBuilder::pluginstakes aPluginSetfrom the plugin API crateseleniumbase-rs-plugin, re-exported asseleniumbase_rs::plugin_api.Browser::launchchecks the set, letsbefore_launchchange the arguments, preferences, extensions, proxy and headless mode, and callson_browser; every tab the library attaches to gets the plugins’ packages andon_page_created;Browser::close(or the last drop) callson_close.Browser::connect_with_pluginsdoes the same for a running browser. A plugin that refuses or fails stops the launch or the tab with the newSeleniumBaseError::Plugin. Guide:docs/tutorials/plugins.md. -
LaunchOptionsBuilder::extensionloads an unpacked extension over the protocol (Extensions.loadUnpacked, which branded Chrome 137 and later need instead of--load-extension), andLaunchOptionsBuilder::preferencemerges a preference into the profile’sDefault/Preferencesbefore the browser starts. -
Browser::launch_mocked(featuretest-util) runs the launch steps, plugins included, against aMockCtrl, whichMockCtrl::newnow creates for a test to script first. -
Feature
tokio-console(off by default):sbrs,sbrs-mcpand programs that use the crate’s logging serve tokio-console for watching async tasks. Build withRUSTFLAGS="--cfg tokio_unstable".
Downloads: Release notes · Source (.zip) · Source (.tar.gz)
Changed#
- The binaries are renamed so they no longer clash with Python
SeleniumBase, which installs its own
sbase: the CLI is nowsbrsand the MCP serversbrs-mcp(wasseleniumbase-mcp). Commands and flags are unchanged; replace the name in scripts and MCP client settings, and generate the shell completions again (sbrs completions zsh > _sbrs). The Docker image runssbrsas usersbrs.sbase_config.tomlkeeps its name.
Added#
sbrs completions figwrites a Fig completion spec, the format of withfig/autocomplete that Warp’s built-in completions, inshellisense and Amazon Q use. Warp also uses the shell’s own completions by default.sbrs completions usageprints a usage (KDL) spec for jdx/usage and mise, kept incompletions/sbrs.usage.kdland checked against the CLI by a test.- Warp Workflows for common
sbrscommands in.warp/workflows/. - Every docs page has a title in front matter for Docs7; the mdBook build strips it.
- ADR 004 proposes a plugin framework for community crates, in the style of puppeteer-extra and Zorilla.
Downloads: Release notes · Source (.zip) · Source (.tar.gz)
Windows fixes from a second round of runs on a Windows 11 PC (Chrome 154 and 155, Edge 155, Firefox 153). There, formatting, clippy, the whole test suite, the real-Chrome tests, Edge, Firefox and the Tauri 3 installer build now pass.
Fixed#
- The browser version on Windows comes from the
chrome.exe(ormsedge.exe) that really runs, read from its file version: while an update waited for a restart, the newest version folder named a Chrome that was not running yet and the wrong chromedriver was fetched. The folder rule also skips a waiting update. - Download folders are handed to Chrome as plain
C:\...paths, not with the\\?\prefixcanonicalizeadds on Windows; ablob:download was cancelled. - The tests that list processes with
pgrepskip those checks on Windows.
Added#
rust-toolchain.toml, so rustup uses Rust 1.99 in the repository by itself.- The Windows status in
docs/platforms.md. Known gap: on Windows nothing ends a browser whose program was killed (macOS and Linux have a process-group watcher).
Downloads: Release notes · Source (.zip) · Source (.tar.gz)
The first release, tagged v0.1.0 on GitHub (not published to crates.io).
Windows is being verified: the first runs on a Windows PC found the problems
listed first under Fixed.
Added#
- geckodriver and msedgedriver: a Firefox session starts geckodriver and an
Edge session msedgedriver itself, as Python SeleniumBase does, where both
used to be handed chromedriver. Each is looked for in
downloaded_drivers/and on thePATH, and downloaded when missing: geckodriver’s newest GitHub release, and the msedgedriver of the installed Edge’s major version frommsedgedriver.microsoft.com(LATEST_RELEASE_<major>_<OS>); an msedgedriver that does not fit the installed Edge is replaced, andwith_driver_versionpins one. Downloads are capped at 64 MB, unpacked (zip or tar.gz) without writing outside their folder, and made runnable as chromedriver is. Both are started with the orphan guard and the macOS re-sign retry; geckodriver gets--websocket-port 0so Firefox sessions can run side by side. Firefox sessions now also sendbrowser_binary_pathasmoz:firefoxOptions.binary,user_data_diras-profile, and the user agent and locale as preferences.sbase installtakes a driver and a version (sbase install geckodriver 0.36.0,sbase get edgedriver), andsbase getis its alias. Checked on macOS with a real headless Edge 155 and the msedgedriver the crate downloaded (tests/edge_msedgedriver.rs); the crate also found and started Homebrew’s geckodriver there, and downloaded geckodriver 0.37.1 and 0.36.0. A headless Firefox 153 ESR took the newmoz:firefoxOptionsthrough the geckodriver of thedocker/firefox-checkcontainer (tests/firefox_geckodriver.rs); the crate itself ran on the Mac, not in the container. - Playwright and Puppeteer importers:
sbase import-playwright(aliasimport-pw) converts@playwright/testfiles,playwright codegenoutput and pytest-playwright, sync and async Python scripts;sbase import-puppeteer(aliasimport-pptr) converts Puppeteer scripts, jest-puppeteer and Mocha tests and Chrome DevTools Recorder JSON exports. Both write one#[tokio::test]per test on the pure CDP engine, keep every statement they do not convert as aTODOwith a diagnostic naming its line, and read the input with a bounded hand-written parser whose text reaches the Rust only in escaped literals and comments (utilities::importer,docs/playwright-migration.md,docs/puppeteer-migration.md).SelectorBuf::role,text,labelandhas_textapproximategetByRole,getByText,getByLabelandfilter({ hasText })in XPath. - Pure CDP engine (
sb_cdp):Browser,Page,Locator, input, cookies, storage, window and emulation, retrying assertions, same-origin frames, CAPTCHA solving, a mock browser for tests without Chrome, request interception (Page::intercept), per-context proxies with per-session passwords, a WebRTC leak shield, andPage::human(curved pointer paths, human typing pace).BrowserPool,Lease,BrowserContextandSessionStoregive bounded, isolated, recycled browsers with in-memory session sharing. - MCP servers:
seleniumbase-mcp --server cdp|driver|sb(24, 26 and 88 tools, plus 8 stealth tools onsb), using the upstream tool names. - MCP developer tools on
cdpandsb:inspect_page,suggest_selectors,diagnose_selector,get_console_messages,get_failed_requests,audit_page,capture_debug_bundle(redacted),export_session_as_test(aBaseCasetest from the session’s steps) anddiagnose_environment. - Failure reports: a failed MCP tool call is answered with its kind, the call, the page’s state (URL, title, ready state, script errors, selector matches; read within two seconds), the hint and the next tools to try, as text and structured content. Mistyped choices and tool names get a did-you-mean.
- Errors for people: every
SeleniumBaseErrorhas a hint, element errors point out a likely mistake in the selector,exit_code()maps categories to exit statuses, andsbaseprintserror:,caused by:andhint:lines.FailureDetailsputs the kind, hint, page and evidence links of a failed test into the JSON and HTML reports (ReportPlugin::with_evidence). - Fingerprints:
Fingerprint::randomized(os, seed)draws a coherent machine and place for all fiveOsTypes;LaunchOptionsBuilder::fingerprintandPage::apply_fingerprintapply one to the Pure CDP engine. - Test plugins and storage:
plugins::observer::{Plugins, TestPlugin}with screenshot, page-source, report and result-store plugins; with thetursofeature,ResultStore,ProfileVault(AES-256-GCM) andsbase report. sbase:encrypt/decrypt,test(carries options into tests asSB_*variables),grid,install,--proxy-list, a typed recorder that refuses what it cannot generate, anddoctoroutput that compares the chromedriver’s version with Chrome’s.- Chromedriver matching:
browser::driver_versionanddownloader::download_chrome_driver_for(major). The launcher downloads the driver of the installed Chrome’s milestone when the cached one is for another major version. ActionRecorder::try_record(reports why an action cannot be generated, with the closest known action),pw_new_pageandpw_closefor the Playwright feature,extract_extension, and iteration onDriverStack.- Parity tooling:
parity/api.toml,parity/upstream.toml,docs/parity.mdanddocs/UPSTREAM_SYNC.md. - Browsers: launch options for Brave, Comet, Opera, Safari, Internet
Explorer, Chrome for Testing and
chrome-headless-shell, typed launch switches, capability overrides, Firefox preferences and CDP event capture (browser::options).sbase installpicks the chromedriver version. RunSettings(BaseCase::set_run_settings): demo mode, slow and fast runs, message duration, skipping JavaScript waits, recordingsleep, and resetting a visual baseline;ActionRecorder::to_gherkinandsave_recording_as_featureexport a recording as a Gherkin feature.sbase translatewrites a JSON scenario’s actions in another language, and the pure CDPPagegainedpost_message,activate_messengerandset_messenger_theme.sbase caseplansandsbase behave-guias terminal apps (featuretui, aliasescase-plansandgui-behave): the Python Case Plans Generator and Behave Commander without Tkinter. The first lists the Rust test functions under a folder and creates, edits and savescase_plans/<test>.mdfor each, and writescase_summary.md; the second lists.featurefiles and their scenarios, runs the marked ones (or all) through the behave runner with the built-in steps, with a tag filter, and shows PASS, FAIL, UNDEF and SKIP per scenario. The state of each is a pure state machine tested with plain values, and the screens are tested as exactTestBackendtext. Withouttuiboth print a hint and exit with status 2, ascommanderdoes. Breaking: the earlier file-writing behaviour moved tosbase caseplans --starter(theCASE_PLANS.mdchecklist) andsbase behave-gui --new(the prompt that writes a feature file), which work in every build.Page::gui_click_captcha(featuregui): clicks a CAPTCHA with the operating system’s pointer, along a human path, at the screen point worked out from the window (Browser.getWindowForTarget), the viewport offset, the widget frame and the vendor’s checkbox offset;Page::gui_captcha_plangives the point without moving the pointer. Tested with unit tests and a mocked browser only: it has not moved the real pointer or been tried on a live CAPTCHA.- Parity: every API in
parity/api.tomlnow has a status (888 implemented, 383 composed, 94 not-applicable with a reason, none planned). Two audits corrected it: 26 entries marked done, and 34 composed or not-applicable ones (see below). --esc-end(--esc_end,--ee):BrowserConfig::with_esc_end,Settings::esc_end,SB_ESC_ENDandsbase --esc-end.BaseCaseputs a key listener in the page; once ESC is pressed,open,click,click_link_text,highlight,wait_for_element_visible,wait_for_ready_state_completeand the typing calls returnSeleniumBaseError::Skipped, whichrun_browser_testcounts as a skipped test.--rec-sb-cdp:ActionRecorder::to_cdp_script,to_cdp_testandsave_recording_as_cdp_testwrite a recording as a pure CDP program (PageandLocatorcalls) for 51 recorded actions; any other becomes a// skipped:comment that quotes itsBaseCasecall.BrowserConfig::with_rec_sb_cdp,Settings::rec_sb_cdp,SB_REC_SB_CDPandsbase --rec-sb-cdpmakeBaseCase::export_recording_as_rustandsave_recording_to_logswrite it, andBaseCase::export_recording_as_cdp_rustasks for it by name.tests/recorder_cdp_compiles.rscompiles the program written for every supported action.--reuse-session(--reuse_session,--rs) and--rcs(--reuse-class-session,--reuse_class_session):BrowserConfig::with_reuse_session,with_reuse_module_sessionandwith_reuse_class_session(name),Settings::reuse_sessionandreuse_class_session,SB_REUSE_SESSION,SB_REUSE_CLASS_SESSIONand thesbaseflags. A process-wide registry (browser::reuse) keeps each shared browser’s WebDriver server, session id and chromedriver;BaseCase::newleases an idle browser started with the same configuration and attaches to it with a client made in its own Tokio runtime (each#[tokio::test]has one), after cleaning it: other windows closed, every cookie and the open page’s storage deleted,about:blank.BaseCase::quitgives it back. One test holds a browser at a time; a second one at the same time gets its own. Idle browsers close with their chromedriver afterreuse::idle_timeout(5 s,SB_REUSE_IDLE_SECS), atseleniumbase_rs::shutdown_reused_sessions()and indrain_cleanups; on Unix the last one also ends with the test binary (see the process-group watcher under Fixed), on Windows it is left running.--rcsshares a browser per Rust module, read from the test thread’s name.BaseCase::session_idandBrowserSession::session_idare new.httpandbytesare now regular dependencies (they were optional, forimpersonate). Checked against a mock WebDriver and in Chrome 155 (tests/reuse_session_chrome.rs: two#[tokio::test]share one Chrome, the first test’s cookie and storage are gone in the second, and the reaper stops chromedriver after the idle timeout).- WebDriver ad blocking:
BrowserConfig::ad_blockmakes a Chromium WebDriver session (Chrome, Chromium, Edge, Brave, Comet, Opera, Chrome for Testing,chrome-headless-shell) refuse the requests of ad and tracker hosts. When the session connects, reconnects or switches to a window, it sendsNetwork.enableandNetwork.setBlockedURLsthrough chromedriver’s DevTools channel. The hosts are the built-inAdBlockList, or a list of your own set withBrowserConfig::with_ad_block_list(new, as iswith_ad_block), which replaces it.AdBlockList::bounded_url_patternsleaves out a host whose parent domain is listed and refuses a list that needs more thanMAX_URL_PATTERNS(2,000) patterns, before any browser starts; anAdBlockListreads and writes as an array of hosts.BrowserSession::apply_ad_blockrepeats the block for a window reached some other way. Firefox, Safari and Internet Explorer have no DevTools channel through WebDriver and block nothing (a warning says so). Checked in a real Chrome 155 (tests/ad_block_chrome.rs): a page’s own requests to listed hosts never reach the server; a frame of another site still loads and makes its own requests; a URL that names a port is not matched. The nine ad-block entries of the parity manifest moved fromplannedtocomposed. - Parity audit of the composed and not-applicable entries, the Rust that
came out of it:
- Text selectors:
SelectorBuf::containing_text(text, tag), and a CSS selector with SeleniumBase’s:contains("text")runs as XPath inPage::locatorand in the WebDriverByconversion. Page::goto_if_not_url,Page::active_elementandactive_element_css;Page::evaluatedrops areturnthat starts the last line, as Python does.Locator::all_waiting(waits for a first match),viewport_rect(window coordinates, asget_element_rect),click_and_holdandflash_with.Browser::debugging_host(andhttp_urlfollows it, so an attached remote browser reports its own host),grant_permissions_for(origin, ..)andgrant_all_permissions;Permissionnow has the 23 kinds Python grants, andPermission::name.sbase version, andsbase obfuscateandunobfuscateas aliases ofencryptanddecrypt.- Real-Chrome tests for these in
tests/sb_cdp_parity_chrome.rs, mock tests intests/sb_cdp_parity_mock.rs.
- Text selectors:
- Stealth scraping, the wire layer (feature
scrape,src/wire): parsers for TLSClientHellos, JA3, JA4 (withJA4_rand the original-order variants), peetprint and Akamai HTTP/2 strings;TlsSpec,H2SpecandHeaderSpec, which say what BoringSSL and wreq can send and refuse the rest by name; the measured tablewire::emulation, with one entry, Chrome 155.0.8059.40 on macOS (any other browser and system isNotMeasured); and the capture tools that write it,capture_tlsandcapture_hints(scrape) andcapture_h2(impersonate). - Stealth scraping, the impersonating client (feature
impersonate):ImpersonationProfile,impersonation_client(wreq on BoringSSL) andCustomProfileBuilder, where every part (JA3,JA4_r, peetprint, signature algorithms, Akamai string, header names, order and values, identity) can be supplied and what cannot be sent is refused with a named error before a client exists; andimpersonate::tls_server, an in-memory CA that issues one leaf per host for loopback TLS. Known gaps against Chrome 155: thetrust_anchorsextension, the ML-DSA signature algorithms and a GREASE signature algorithm. - Stealth scraping, the scrape layer (feature
scrape,src/scrape): an RFC 6265 cookie jar with public-suffix checks;StealthIdentityand its owner-only identity file;SessionPolicy; challenge detection for seven vendors and a mitigation policy; a proxy pool with locales by exit country; per-site state with an atomic JSON store; redacted scraper events; andBrowserScraper, which fetches through a real Chrome on the pure CDP engine, solves challenges and rotates egress and profile.Fingerprint::randomized_with_majorfixes the Chromium major an identity claims. HttpSession(featureimpersonate): plain HTTP requests that continue a browser page’s session with the measured TLS, HTTP/2 and headers of Chrome 155 on macOS. It reads the cookies, user agent, languages and proxy from a page (HttpSession::from_page) or an identity, keeps an RFC 6265 jar, follows redirects by hand, bounds time, bodies and pace, judges replies with the mitigation and session policies, and writes cookies back to the page. A page of another Chrome major, one that saysHeadlessChrome, or an unmeasured system is refused. The first request to an HTTP/1.1-only origin carries an HTTP/2priorityheader.MitmProxy(featureimpersonate): a loopback proxy that ends a real Chrome’s TLS with the ephemeral CA and replays each request through the impersonating client, behind a random token, with bounded connections, heads and bodies, and a hot-swappable egress and profile. Upgrades other than WebSocket are refused, and a request claiming another Chrome major is refused. Measured on Chrome 155 (headless, macOS): a chain carrying the pinned leaf key is accepted by a Chrome that has the SPKI-list switch and no proxy, and is never served to a page behind the proxy.- WebSocket through
MitmProxy: awss://handshake goes upstream over HTTP/1.1 with the measured WebSocketClientHello, the101is relayed, and the bytes are copied both ways with idle and per-direction byte bounds (MitmOptions::websocket_idle_timeout,max_websocket_bytes); half upgrades and other protocols are refused (MitmError::MalformedUpgrade,NotIntercepted). - The
MitmProxypriming page: a WebSocket may now be a tab’s first request through the proxy. Chrome never shows a WebSocket’s407toFetch(measured on Chrome 155, tab and browser target alike), soconfigure_launchmakesMitmProxy::prime_url()(https://sb-mitm.invalid/and a random path) the start page; the proxy answers it itself behind the token, and Chrome keeps the credentials for the browser context. A context created later loads the page itself. Anything else for that host is an empty404; nothing for it goes upstream.MitmStats::primedcounts the loads. ws://throughMitmProxy: Chrome sends aws://WebSocket to an HTTP proxy as aCONNECTwhose tunnel carries the handshake in plain text (measured on Chrome 155). The proxy now reads a tunnel’s first bytes: a TLS handshake is ended as before, and a plain-text WebSocket handshake takes the checks, header rewrite, bounds and relay of awss://one, upstream ashttp://. Any other plain text in a tunnel is still refused and never relayed.StealthSession(featureimpersonate): the session that moves betweenHttpSessionand a real Chrome behindMitmProxyby the mitigation and session policies. It starts the proxy when a browser is first needed, makes up to 4 attempts per fetch by default, and rotates the profile and the egress in both modes. Tested against loopback sites, a scripted browser and, in ignored tests, a headless Chrome 155 on macOS; not against a live service.- Re-exports: the main types of the stealth layer at the top of
wire,scrapeandimpersonateand at the crate root behind the same features. The TLS and HTTP/2SpecErrorenums areTlsSpecErrorandH2SpecError. The stealth layer is experimental and not a stable API. docs/tutorials/stealth_scraping.mdanddocs/adr/003-stealth-scraping-layout.md.- A
rust-analyzer.tomlthat loads the optional features so analysis coverssrc/mcpandsrc/scrape. CLAUDE.md(a map of the modules, checks and gotchas),CHANGELOG.md, anddocs/design/stealth_parity_plan.md(the plan for the stealthscraper-rs feature set and JA3/JA4/Akamai impersonation).- Real referrals:
BaseCase::generate_referral(start_page, destination_page, selector),generate_traffic(.., loops, selector),generate_referral_chain(pages)andgenerate_traffic_chain(pages, loops)open the start page (or use the current one), build a link to the destination in it with a script and click it, so the destination receives a realReferer, as Python does. The four parity entries areimplemented. Mock tests assert the order of the commands and that the URL stays inside its string literal;tests/generate_referral_chrome.rs(ignored) has a Chrome 155 visit a loopback site that records theRefererit receives. BaseCase::set_content_to_frame(frame)replaces the page with the content of an iframe on it, selected or named, as Python does; an iframe whosesrcis a web or file address opens in a new tab instead.set_content_to_defaultbrings the page back (all swaps at once) andset_content_to_parentundoes one swap. With nothing swapped they still only leave the frame, as before. Tests against a fake browser behind the mock WebDriver cover the swap, the tab, nested swaps, a swap the page forgot and a missing frame;tests/set_content_to_frame_chrome.rs(ignored) does it in Chrome 155. The parity entry isimplemented.- Downloads on the pure CDP engine:
Page::download_file(url, dir)has the page fetch an address and save it,download_by_click(selector, dir)follows the download a click starts,save_file_as(url, path)saves at an exact path andassert_downloaded_file(name, timeout)waits for a file in the folder the browser saves to; each returns the final path. They follow the browser’s own download events (Browser.setDownloadBehaviorwith events), so a cancelled download, a bad HTTP status or a fetch the page may not make isSeleniumBaseError::Download, and a download that does not begin or end in time isWaitTimeout.Browser::download_dirsays where downloads go. The three parity entries areimplemented. Mock tests are intests/sb_cdp_download.rs, Chrome 155 tests intests/sb_cdp_download_chrome.rs. - Firefox add-ons:
BaseCase::install_addon(path, temporary)installs an.xpifile or an unpacked add-on directory through geckodriver’smoz/addon/installcommand and returns the add-on id, anduninstall_addon(id)removes it withmoz/addon/uninstall. Any other browser getsSeleniumBaseError::Unsupported; an empty path, one with..in it, one that does not exist and one that is neither an.xpinor a directory areInvalidConfig, before anything is sent. The parity entry isimplemented(it wasplannedand always returnedUnsupported). Unit tests assert the exact endpoint and JSON body against the mock WebDriver. The same bodies were sent withcurlto a real geckodriver 0.37.1 and Firefox 153 ESR in a Docker container (docker/firefox-check, which holds nothing of this crate): both an unpacked directory and an.xpiinstalled withtemporary: true, the add-on’s content script ran, and the uninstall stopped it.tests/install_addon_firefox.rs(ignored) runsBaseCaseagainst that geckodriver and installs and removes an add-on.
Changed#
examples/tauri-profile-manageris on Tauri 3:tauri3.0.0-alpha.4,tauri-build3.0.0-alpha.3,tauri-plugin-opener3.0.0-alpha.2and@tauri-apps/cli3.0.0-alpha.4, pinned exactly. No code had to change; on macOS its 115 tests pass and the Tauri 3 CLI builds the.app(the window was not opened in that check). One test of the example had gone stale whenEmulation::localestarted setting the language too; it now checks the last user-agent override.- The
Dockerfilelinks Debian’schromedriverasdownloaded_drivers/chromedriverin the working directory, so the WebDriver engine in the image starts a browser with no network instead of downloading a driver, and its builder stage takes aCARGO_BUILD_JOBSbuild argument for small machines.docker/Dockerfile.verifyanddocker/verify-linux.shrun the test suite on Linux in a container (the sources mounted read-only, the build output in a volume, a virtual display for the headed tests). The suite, run that way on linux/arm64 with Chromium 154, is indocs/platforms.md. sbaseexits with a status that follows the error’s category instead of always 1, and printserror: ...instead of[ERROR] ....TestResulthas afailurefield, andPageEvidencegainscurrent_urlandpage_title(with defaults).- The
cdpMCP server no longer offerstag:contains("text")selectors, which its engine never supported;diagnose_selectorgives the XPath for one. (A:contains()selector now does run, converted to XPath, inPage::locator.) - Breaking (parity audit):
BaseCase::set_time_limitnow fails the test once it has run longer than the limit, at its next open, readiness wait or element action, and cuts waits to the time left; it used to only cap single waits.set_default_timeoutraises or lowers the default wait (1 to 60 seconds) instead of setting the time limit, andreset_default_timeoutrestores it.BaseCase::newopensstart_pagewhen the configuration has one.run_browser_test(andPlugins::run_browser_test) count a test that ends inskip_testas passed and log the reason.Emulation::localealso setsnavigator.languageandAccept-Language, keeping the user agent and client hints.Permission::ALLhas 23 entries instead of 7 andPermissionhas more variants. The parity entries for--esc-end,--rec-sb-cdpandgui_click_captchaare nowplanned(so were the ad-block options, until the WebDriver ad block above); the notes of many composed entries say what differs from Python. - The pure CDP engine runs
session_registry(27 providers: the default registry withoutpluginsandconnection), so a page reads Chrome’s ownnavigator.plugins,mimeTypes, PDF viewer flag andnavigator.connection;navigator.userAgentDatais defined only in a secure context; and a desktop persona’s screen is sent as a0by0device-metrics viewport plusscreenWidthandscreenHeight, so the page keeps its real viewport.default_registrystill holds 29 providers. - Requires Rust 1.99 (
rust-version, theDockerfilebase image and the CI msrv job);zipmoved to 9.0 and the lockfile is current. - Breaking:
BaseCase::generate_referral,generate_traffic,generate_referral_chainandgenerate_traffic_chaindo what the Python methods do (see Added). Their old forms returned or opened a?ref=<referrer>URL, which sends noReferer; the old(url, referrer)signatures are gone. - Breaking:
BaseCase::set_content_to_frametakes the iframe to show (a selector or a name), not HTML to append as a new iframe; there is no replacement for the old behaviour, whichset_contentandexecute_scriptcover. - Breaking:
ad_blockin a WebDriver launch no longer passes--blink-settings=imagesEnabled=false. That turned every image off and blocked no ad;with_block_images(true)is the setting for it. BaseCaseactions wait for their element (up toset_timeout, 10 s by default) and fail withWaitTimeoutnaming the selector.- Breaking, from a parity audit (each method now does what the Python
method of the name does):
press_keys(css, text)types text into an element (the old one-named-key method ispress_key(name));select_all(css)returns the matching elements (the old select-every-option method isselect_all_options);bring_to_front(css)raises an element (the window focus isbring_active_window_to_front);console_log_script(script)takes the expression to log;deferred_check_window(name, level)anddelayed_check_window(name, level)take a name and a level;assert_downloaded_fileisasyncand waits for the download;switch_to_frame_of_elementreturns the frame’s name;find_text,find_exact_textandfind_non_empty_textwait for visible text and fail with a timeout instead of returningNone;DeferredAsserthas three more variants.check_windowat level 0 no longer fails on a difference. - Breaking:
check_windowsaves and compares the page’s tag structure, as Python does, not the screenshot byte for byte. The baseline is the foldervisual_baseline/<test>/<name>(page_url.txt,baseline.pngandtags_level_1.txttotags_level_3.txt); levels 1 to 3 compare the tags, the tags with their attribute names and the tags with their attribute values, and a level other than 0 to 3 is an error. A baseline saved asvisual_baseline/<name>.pngis not read; the first run saves a new one. BaseCase::generate_referral,generate_trafficand their chains,set_content_to_frameandinstall_addon, the--reuse-sessionoptions, thecaseplansandbehave-guicommands and three CDP downloads were markedplannedinparity/api.toml, because the Rust code only looked like the Python one. The referral helpers,set_content_to_frame,install_addon, the CDP downloads, the--reuse-sessionoptions, thecaseplansandbehave-guiterminal apps andgui_click_captchahave since been written (see Added), so no entry isplannednow.objectifyis not applicable andmkrecis mapped ontosbase record.- Waits report the error that stopped them, and link checks report real status codes.
Cookies::savewrites JSON, not Python pickles. The oldseleniumbase-mcptool names were replaced by the upstream names.- Breaking:
SeleniumBasePlugin,PluginManagerandDbReportingPluginwere removed (nothing called the manager);ElementApi::find_elementwas removed from the trait;From<AsyncStepRegistry> for StepRegistrywas removed;common::{decorators, exceptions}remain as#[deprecated]shims overutilities::retry. - The real-Chrome tests of
MitmProxy(tests/impersonate_mitm_chrome.rs) no longer all skip off the measured system: only the one that compares the browser with the measured persona does. The pin, loopback and SSRF refusal and WebSocket tests run on any Chrome or Chromium, one of another major being started with the profile’s user agent. On linux/arm64 with Chromium 154 (docker/Dockerfile.verify) seven of the eight now run and pass.
Fixed#
- Windows, found by the first runs on a Windows PC: every
sbasecommand crashed at start with a stack overflow (a debug build needs more than the 1 MB a Windows main thread gets;sbasenow runs on a 16 MB thread, andtests/cli_e2e.rsstarts it under a 1 MB stack); the chromedriver for an older Chrome was never fetched, becausechrome.exe --versionprints nothing there (the version now comes from the folder beside the program, as in Python SeleniumBase); a driver whose session failed to start kept running (a droppedDriverProcessnow ends it); text files were checked out with CRLF (.gitattributesmakes them LF) and the parity scripts wrote CRLF; and four tests expected/in printed paths. - The
playwrightfeature no longer needs Python.rustwright-corewas a direct dependency with its defaultpythonfeature, which adds Python bindings through pyo3, so every program built withplaywrightlinkedlibpythonand failed to start without it (dyld: Library not loaded: @rpath/libpython3.14.dylib). It is now used withdefault-features = false, asrustwrightitself does: pyo3 is gone from the dependency tree, the 54playwrightlibrary tests pass, and the test binary links nolibpython. - Browsers no longer outlive the program that started them. A chromedriver,
the Chrome it starts, and a Chrome launched by the pure CDP engine kept running
when the program that started them was killed (an interrupted test run, a
stopped MCP server) or, for
--reuse-session, simply ended. On macOS a leftover Chrome could hold the person’s own profile, so the Chrome they opened closed at once. On Unix each launched driver or browser now joins a process group led by a small/bin/shwatcher that reads a pipe this program holds; when the program ends, by any means, the watcher sends the group SIGTERM and then SIGKILL. Checked by killingsbaseandseleniumbase-mcp --server cdpwith SIGKILL while Chrome 155 was open (nothing was left), and by unit tests, including one whose parent process is killed. Windows is not covered. sbase cdp,cache-clear,throttle3g,cdp-click-element,cdp-type-text,print-pdf,pdf-textandassert-pdf-textfailed with “CDP is unavailable” unless--cdpwas given; with no mode asked for they now use cdp mode on a Chromium browser.screenshot --pathandsave-source --pathwrote intolatest_logs/while printing the path given; they now write that path.run-scenarioexited 0 when a step failed; it now exits 1. Text read from a PDF spells the ligatures U+FB00 to U+FB06 as their letters, so “fixture” printed by Chrome with an fi glyph is found. Found by the newtests/cli_e2e_chrome.rs, which runs the browser commands as the real binary on headless Chrome.sbase decryptwith the wrong passphrase saidunsupported operation: decryption failed: ring::error::Unspecifiedand advised on browsers and engines; it now says the passphrase inSB_ENCRYPTION_KEYis wrong or the token was modified (exit status 2 as before).sbase import-pythonwith a file it cannot read said onlyNo such file or directory (os error 2); it now names the file.tests/cli_e2e.rsruns every subcommand’s--helpand the offline commands.- Log lines went to standard output, in colour even when the output was a file or
a pipe.
seleniumbase-mcptherefore wrote them into the JSON-RPC stream (three lines in a session that did nothing but connect), which a client reads as corrupt messages, andsbase get-title | ...carried the logs with the answer. Theinit_tracing*functions now write to standard error, with colour only on a terminal and not whenNO_COLORis set.tests/mcp_stdio_chrome.rsspeaks JSON-RPC to the real binary and fails on any line that is not a message. - The three MCP servers answered
initializewith the server namermcpand the library’s version (3.5.1), so a client listedcdp,driverandsbas the MCP library. They now reportseleniumbase-cdp,seleniumbase-driverandseleniumbase-sbwith this crate’s version. sbase open,smokeand the other commands that drive a browser ignoredSB_HEADLESSand the otherSB_*settings variables unless--configwas given, and ignoredSB_WEBDRIVER_URLandSB_CHROME_BIN; in a container that made--headlessmandatory. They now start from the config file, apply the variables on top, and then the command-line options, so an explicit--headlessor--headedstill beats the environment.--webdrivernow defaults toSB_WEBDRIVER_URL, thenhttp://localhost:4444, and a bad variable value (SB_HEADLESS=maybe) stops the command with an error before a browser starts.tests/cli_env.rsruns the real binary against a fake WebDriver server.- The chromedriver download asks Chrome for Testing for the
linux-arm64build on an arm64 Linux machine (a Docker container on an Apple-silicon Mac, or on Graviton) instead of the x86-64linux64one, which cannot run there: the WebDriver engine failed with “chromedriver exited as it started”. The build exists from Chrome 153 on; on Linux machines of any other architecture the error now says that no build is published and where to put a driver. - The real-Chrome tests that compare a browser with the measured Chrome 155 on macOS
(
impersonate_capture_h2_chrome,impersonate_http_chrome,impersonate_mitm_chrome,stealth_session_chrome) say they are skipped and pass on a system that has no measurement (Linux, Windows), instead of failing onNotMeasured. A macOS machine whose Chrome has another major version still fails them: that is the drift check. - The wire test
a_custom_ja3_arrives_exactly_as_givenfailed on about two runs in five, on any system: BoringSSL pads a ClientHello of 256 to 511 bytes up to 512 with a padding extension, and the “classical groups only” variant falls on either side of that limit with the random size of the ECH GREASE payload. The test now asserts that rule (a padded hello is 512 bytes, an unpadded one is not in the range) and compares the JA3 and JA4 without the padding. BrowserSession::reconnectstops the chromedriver of the previous session when the new one starts its own, souc_open_with_reconnectno longer leaves one chromedriver running per call.js_click,js_type,set_attribute,remove_attribute,scroll_to, the localStorage helpers and every otherBaseCasepage script quote their selectors and values with the crate’sjs_code::quote; a quote, a newline or U+2028 in test data could break out of the string. Waits that run out of time (wait_for_title,wait_for_attribute,wait_for_exact_text, the link-text waits,wait_for_any_of_elements_present,download_file) returnWaitTimeoutinstead ofAssertionFailedand name what they waited for.wait_for_ready_state_completetakes its default from the configured timeout.BrowserSession::wait_for_element,wait_for_element_visible,wait_for_element_clickable,wait_for_element_absentandwait_for_textreturnWaitTimeout, notAssertionFailed, when the time runs out, and so do theBaseCasemethods that return their error as it is (wait_for_element,wait_for_element_visible,wait_for_element_absent,wait_for_text,find_element,choose_file,slow_click,get_select_optionsandget_shadow_root). The message names the locator (CSS(#a)) or the text, and the seconds waited. Code that matchedAssertionFailedon these waits must matchWaitTimeout.CdpPage(the DevTools page of a WebDriver session) quotes the selectors, attribute and property names, option texts and values it puts into its page scripts withjs_code::quote, asBaseCasedoes. It escaped only the backslash and the single quote, so a newline or U+2028 in a value (the text of a<textarea>) was a syntax error in the script.- The examples run offline against a loopback site (
examples/local_site) and check what they claim; thepartial link=selector prefix in the selectors tutorial ispartial_link=. - The native pointer helpers of the
guifeature (gui_click_element,gui_click_with_offset,gui_drag_and_drop,gui_move_to_element,gui_hover_and_clickanduc_gui_handle_captcha) aim at the element’s place on the screen. The script that finds it scrolls the element into view and adds the browser toolbar, the window borders and the offset of each same-origin frame the element is in; it used to add only the window’s position, so a click in a headed window landed a toolbar’s height too high.uc_gui_handle_captchawaits for the element to be visible first. Browser pixels are taken to be screen pixels. get_domain_urlkeeps a port that is not the scheme’s default (http://localhost:8080), as Python’s does; it used to drop it.wait_for_element_presentlooks for the element once before it gives up, so an element that is already there is found with a timeout of zero.assert_is_instanceandassert_not_is_instancelook at the type of the value passed. They read the name of a&dyn Any, which is alwaysdyn core::any::Any, so the first never passed and the second never failed. They take&Tnow; a call that passes&valuecompiles as before.xpath_to_css(soconvert_xpath_to_cssand the Python importer) converts only what has an exact CSS equivalent, a tag or*with attribute tests joined byand, and returnsInvalidSelectorfor the rest. It used to drop a predicate it did not understand://div[2]becamedivand anorbecame an AND. An id with a backslash or a line break no longer produces invalid CSS inSelector::Id, and..and../count as XPath.normalize_page_url(open,Page::goto, the scrape and impersonation fetches) matches schemes without regard to case, soHTTPS://EXAMPLE.COMis left alone, and refuses a file path and a scheme a browser cannot open (ftp:,mailto:,javascript:) with a message that says so, instead of turning them into anhttps://host.Page::close(pure CDP) waits, for at most two seconds, until the browser no longer lists the tab;Browser::list_pagesright after a close could still show it, which made a real-Chrome test fail one run in five.sbase objectify,caseplans,behave-gui,mkrec,mkchart,mkpresandprintexit with status 6 and print the cause when they cannot write or read their file; they used to exit 0. The WebDriver-hosted CDP page no longer sendsDOM.enable(it finds elements throughRuntime.evaluate), so no part of the engine enables a detectable domain.- A chromedriver that does not match Chrome (the cause of “chromedriver keeps
crashing”): the launcher,
sbase installandsbase doctornow agree with the installed Chrome, and a replaced driver is written as a new file because macOS kills a signed binary overwritten in place. - The stealth layer no longer enables the
RuntimeCDP domain; the proxy-auth extension is one Chrome can load; the page-script stubs were replaced with working scripts (checked in a real Chrome); Playwright honoursBrowserConfigand keeps web security on. CdpDriversends commands to the tab, not the browser socket;prompt()asks for text; deferred assertions check the page they were made for; chart series line up by label; the presentation escapes its title; the HTML inspector gives each issue a selector that matches its element;get_originkeeps the port; PDFs that need a password can be read.- A WebDriver session with a fingerprint no longer sends the proxy login
(
Proxy-Authorization) to every site as an extra header; the pure CDP path already filtered it. - A flaky Grid test (the stand-in
javawas slow on its first run). - Parity audit:
set_text,set_text_contentandhighlight_update_textset an input’s value instead of its (invisible)textContent; the deferred asserts check at once,deferred_assert_exact_textis exact anddeferred_assert_non_empty_textchecks text;switch_to_frame_of_elementfinds elements in iframes;select_option_by_indexfails for a missing option and fireschange;get_chrome_versionreturns the version number andget_chromium_versionthe major version;is_valid_urlfollows Python’s pattern;ad_blockuses upstream’s selector list;add_js_code_from_linkdownloads and injects the code;post_success_messageand the other toasts go away;highlightrestores the element’s outline;delete_saved_cookiesdeletes the filesave_cookieswrote;console_log_stringwrites to the console. - The recorder generates Rust for every recorded action, keeps events across navigations, and the stdin recorder only says “recorded” for what it recorded.
- The CLI scaffolders no longer overwrite files; the interactive commands stop at
the end of their input and on Ctrl-C; Gherkin runs the way
behavedoes. sbase translateandtranslate_scenariorewrite theactionof the steps in the rootstepslist only, not anactionin any other object three levels down (such asmeta.x.action), which was changed and counted as translated.EgressRef::parselower-cases the host ofsocks5andsocks5hspecs, as it does for http and https, so one proxy spelled two ways has oneEgressKey.ActionRecorder::to_gherkinskips a recordedtype_textwith no text and says so in a# skipped:comment; the writtenI type "" into "..."step matched no handler, so the generated scenario failed at that step.sbase translate --overwritekeeps the file’s permissions (the replacement was left at mode 0600) and writes through a symbolic link instead of replacing it.
Security#
- Chrome switches in a profile payload that run programs, load code or reroute
traffic are refused; file names a caller supplies cannot climb out of their
folder; the HTML report escapes what tests print; proxy passwords no longer
appear in
Debugoutput. ActionRecorder::to_gherkinescapes the name of an action it cannot turn into a step, so a recorded name with a line break can no longer end the# skipped:comment and add a Scenario or steps to the feature.capture_debug_bundleno longer copies what earlier tool calls returned intobundle.json: the result of a successful call is replaced by its length, so a cookie, a stored token or a page’s text read byget_cookies,manage_cookiesor a storage tool is not saved with the bug report.