Browser extensions for seleniumbase-rs, the Rust port of SeleniumBase. One source tree builds Manifest V3 extensions for Chrome (also Edge and other Chromium browsers) and Firefox.
Browser extensions for seleniumbase-rs: a recorder that exports Rust tests, an element inspector, and a Chrome DevTools Recorder export
The extension is written in strict TypeScript and built with Bun; the built dist/ holds
plain JavaScript for each browser.
Start and stop recording from the toolbar popup (or the Chrome side panel, or the Firefox sidebar). Clicks, double clicks, typing (one step per field), selects, checkboxes, key presses (Enter, Escape, arrows), file inputs (the file name only) and, if you turn it on, scrolls become steps you can edit, reorder and delete. Export as a seleniumbase-rs pure CDP Rust test, a BaseCase Rust test, or a Chrome DevTools Recorder JSON file (the @puppeteer/replay format that sbrs import-puppeteer reads).
Pick an element (hover, click; Escape cancels) and see its selectors, each with the number of elements it matches, and calls to copy for both engines (page.locator(...).click(), sb.click(...), …). The Page tab shows what the page sees: navigator.webdriver, user agent and client hints, languages, platform, hardware, screen and window sizes, time zone, WebGL vendor and renderer, a canvas hash, plugin counts, permission states and headless markers, and copies, exports and imports it as the JSON of seleniumbase-rs’s Fingerprint (see The Page tab).
Adds “seleniumbase-rs (pure CDP)” and “seleniumbase-rs (BaseCase)” to the Export menu of the DevTools Recorder panel.
Nothing is sent anywhere. Recordings are kept in the extension’s own storage and leave it only when you press
Download, Copy or Export. The value of a password or payment field is never read, so it cannot be recorded; the
generated code reads it from an environment variable instead (RECORDED_PASSWORD, …).
The Page tab: capture, copy, export, import#
The Page tab reads what the page’s own scripts see and writes it as the JSON of a seleniumbase-rs
Fingerprint (fingerprint API v2, seleniumbase-rs 0.5): one entry per setting, each its value
("timezone": "Europe/Paris", "screen": { "width": 1512, "height": 982 }). A setting the page cannot tell
(the position, the fonts, a proxy, the WebRTC addresses) is left out, which seleniumbase-rs reads as “natural”.
The only word it writes is "off", for client_hints on a secure page of a browser that has no
navigator.userAgentData (Firefox, Safari).
| Button | What it does |
|---|---|
| Capture again | Reads the page again. |
| Copy | Puts the JSON on the clipboard. |
| Export | Saves it as fingerprint-<host>-<date>.json (a Blob link, so the extension needs no downloads permission). |
| Import | Opens a .json file you pick, checks it as API v2, and lists every setting the file claims next to the page’s value as match, differs or not observable (a page cannot read the position, the fonts or a proxy, and a "random" setting has no value until seleniumbase-rs draws one). |
Import refuses the old flat shape of seleniumbase-rs 0.4 and earlier (screen_width, webgl_vendor, flags,
…) with each field named, as well as bad JSON and an "off" where seleniumbase-rs refuses one. The file is read
and compared inside the panel; nothing is uploaded.
In Firefox the toolbar popup can close when the file dialog opens; use the sidebar for Import.
Install from source#
You need Bun (mise install sets everything up).
Build
Load the extension
Open chrome://extensions (or edge://extensions), turn on Developer mode, Load
unpacked, choose dist/chrome.
Open about:debugging#/runtime/this-firefox, Load Temporary Add-on, choose
dist/firefox/manifest.json (or the zip).
Start recording
Press the toolbar button, then Start recording.
The first time, the browser asks to let the extension “read and change your data on all websites”. That is only so the recording can follow links from page to page; say no and it still records the page you started on.
Safari#
Safari is not built or tested: no converter is installed here. The route is Apple’s converter, then Xcode:
Build the generated project and turn the extension on in Safari’s settings. Expect to review the manifest it
reports: Safari has no sidePanel and no DevTools Recorder, and may treat optional host access differently.
Develop#
Layout and extension points#
src
manifest
sharedno browser APIs except api.ts: selector generation, code generation, the step model
codegen
scripts
A feature is a folder with a feature.json. The build finds src/*/feature.json (a folder with
"enabled": false is skipped) and, for each browser it lists ("browsers", both by default):
- bundles
background(a module) into the one background bundle, aftersrc/background/index.ts; - bundles each file of
scriptson its own, as a classic script, todist/<browser>/<feature>/<file>(content scripts, page scripts, a DevTools page script); - bundles
panelViews(a module that callsregisterViewfromsrc/ui/registry.ts) into the panel; - copies every other file (html, css, images) as it is, except
feature.json,*.md, files starting with_; - merges
manifest(andchrome/firefox) into that browser’s manifest. Arrays are joined without repeats, objects are merged, and two different values for one key stop the build, so features cannot silently override one another.
So to add a page, put the .html and its script in the feature folder, list the script under scripts, and
open it with ext.runtime.getURL("<feature>/page.html"); to make it reachable from a web page, declare
web_accessible_resources in the feature’s manifest. To add a permission, put it in the feature’s
manifest (permissions, optional_permissions, optional_host_permissions, …) and add it to the allow
list in scripts/lint.ts on purpose. Static host_permissions and content_scripts are not used: the extension
injects its scripts into the tab you point it at (activeTab), or into any tab once you allow all sites.
Messages (src/shared/protocol.ts): { v: 1, type: "<feature>/<name>", ...payload }, answered with
{ ok: true, ...data } or { ok: false, error }. The panel talks to the background with
ext.runtime.sendMessage; a content script does the same; the background talks to a content script with
ext.tabs.sendMessage. A feature adds a type from its background module:
The router refuses a message from the wrong kind of sender. After a change, broadcast(MSG.STATE_CHANGED, { area })
makes the panel’s ctx.onChange(area, fn) run. Anything a feature keeps goes through src/background/store.ts
(local survives a restart, session does not), and the background must not hold state in memory: the browser
stops and starts it.
Placeholder features#
Two features are disabled placeholders that wait for their code.
bridge: the live bridge to a running seleniumbase-rs process
The live bridge between the extension and a running seleniumbase-rs process. Nothing here yet.
To build it:
-
Change
feature.jsonto describe the feature (remove"enabled": false), for example -
background.tsregisters its message types withregisterHandler(../background/router.ts); message type names start withbridge/and are added tosrc/shared/protocol.ts. -
view.tscallsregisterView(../ui/registry.ts) to add a tab to the panel. -
Any permission is added to the allow list in
scripts/lint.tson purpose, and explained in the README.
See “Layout and extension points” above for what the build does with each field.
converter: the in-browser converter
The in-browser converter (Selenium IDE, Playwright, Puppeteer and Python SeleniumBase recordings to seleniumbase-rs Rust). Nothing here yet.
To build it: turn on feature.json (remove "enabled": false), put the conversion in a module under
src/shared/ if the recorder or the DevTools Recorder export should use it too (they already share
src/shared/codegen/: a converter that produces steps — see src/shared/steps.ts — gets all three export
formats for free), add a panel tab with "panelViews", and register any message types with registerHandler.
See “Layout and extension points” above.
Security#
No remote code, no eval or new Function (the build and the tests check the output), a strict extension
CSP (script-src 'self'), no static host permissions, no content_scripts in the manifest. Page-derived text
is only ever put in the DOM as text, and into Rust only through rustStr. A content script’s messages are
validated and bounded (sanitizeStep) before they are stored.
Limits#
Steps inside iframes and shadow roots are not recorded (a shadow host is recorded as the target). The recorder follows the tab you started in, not other tabs. Only Chrome 155, Edge 155 and Firefox ESR 153.4 were run; see “End-to-end tests” below for what was and was not exercised in a real browser (the toolbar popup, the permission prompt, the side panel and the DevTools Recorder’s Export menu were not).
End-to-end tests#
A Rust crate (sbrs-companion-e2e) that loads the built extension into a real Chrome (and Edge) through
seleniumbase-rs’s pure CDP engine, and a shell check that loads it into a real Firefox in Docker.
The crate also reads the Page tab’s JSON and the shared accepted and refused fingerprint files with seleniumbase-rs 0.5.0, so the extension and the library agree on which files are valid, and drives Export and Import in Chrome.
Run the browser tests one at a time: each starts its own browser and takes a minute or two.
Why dist/chrome-e2e#
The extension asks for host access to all sites as an optional permission, at the moment you press
Start recording, and otherwise relies on activeTab, which is granted by clicking the toolbar button.
A headless browser has no toolbar and cannot answer the permission prompt. dist/chrome-e2e and
dist/firefox-e2e are the same build plus host_permissions for http://127.0.0.1/* and
http://localhost/*; the build test checks that this is the only difference. The tests also load
dist/chrome (without it) and check that it says plainly that it has no access to the page.
How the extension is driven#
- The extension is loaded with
Extensions.loadUnpacked(branded Chrome ignores--load-extensionsince 137; the browser is started with--enable-unsafe-extension-debugging). - A headless browser has no popup. The popup’s page,
ui/panel.html, is opened as an ordinary tab, and the messages the popup’s buttons send are sent from it withchrome.runtime.sendMessage: the same background code answers them. What is not exercised this way is the popup chrome itself (the toolbar button, the permission prompt, the side panel’s opening). - The page being recorded gets real input events from
Input.dispatchMouseEvent/dispatchKeyEvent(Locator::click,type_text,press), which the page sees as trusted events, as a person’s input is.
What each test checks#
parity_with_the_action_recorderBrowser: none
Checks: tests/golden/*.rs.txt equal what ActionRecorder writes; the bun tests hold the extension’s generators to the same files
records_exports_and_replays_in_chromeBrowser: Chrome
Checks: records a flow with real input (typing, a password, a select, a checkbox, a button, a form submit across two pages, a link); the steps; the password never recorded; the three exports; the exported pure CDP test and the BaseCase test compile in a scratch crate; the pure CDP test runs against the same pages (headless) and the pages log the same events as during the recording
inspector_selectors_match_one_element_eachBrowser: Chrome
Checks: picks 21 awkward elements with the real picker (real clicks); every selector it calls unique matches exactly that element in the page; no generated-looking class or id is used; the page report, parsed into seleniumbase-rs’s Fingerprint; the build without host access; the DevTools Recorder plugin’s stringify on a Recorder JSON; hostile text through the bundled generators, and the results compile
edge_loads_the_extension_and_recordsBrowser: Edge
Checks: Edge accepts Extensions.loadUnpacked, the background answers, a click is recorded
firefox/check.shBrowser: Firefox ESR (Docker)
Checks: geckodriver installs dist/firefox-e2e as a temporary add-on; the background runs; the recorder starts and records typing and a click made through WebDriver; the export. WebDriver may not open a moz-extension: address, so the panel page is shown in an iframe of a loopback page (the e2e build lists it as web-accessible for 127.0.0.1), and the fixture tab is found by trying small tab ids
Firefox#
What a headless run cannot show#
The side panel, the Firefox sidebar, the toolbar popup as a popup, the permission prompt, and the DevTools
Recorder panel’s Export menu (the plugin is registered only inside a real DevTools window; the tests call
the registered plugin object). Safari: no converter is installed here; the route is
xcrun safari-web-extension-converter dist/chrome followed by a build in Xcode.
Licence#
MIT.