Extending Browser
JavaScript extensions
Add keywords that run on the Node side with full Playwright access — and attach a debugger to them.
A JavaScript module adds keywords that run on the Node side, with the Playwright
page object in hand. It is the lighter of the two extension points: no Python,
no class, just exported functions.
Browser loads the module with require(). CommonJS is the safe default and what
the library's own tests use, but on the Node versions Browser supports require()
also loads ESM, so export/import works. The one thing it cannot load is an
ESM module using top-level await, which fails with ERR_REQUIRE_ASYNC_MODULE.
A module
Three conventions carry the whole API:
- Exported names become keywords — the export key, not the function's own
name.
exports.getLinks = getLinksgivesGet Links; theexports.myMouseWheel = mouseWheelbelow givesMy Mouse Wheel. - Five argument names are filled in for you, by name — see below.
fn.rfdocbecomes the keyword documentation, so your keyword shows up in Libdoc and in editor tooltips like any other.
The five names Browser fills in
Name a parameter one of these and the library passes the object in. Everything else in your signature becomes an ordinary keyword argument.
| Name | You get |
|---|---|
page | The active Page |
context | The active BrowserContext |
browser | The active Browser |
logger | A function that writes to the Robot Framework log |
playwright | The playwright module itself |
They are matched by name, not by position. Browser reads your parameter
names and fills in the ones it recognises, so mouseWheel(x, y, logger, page)
and mouseWheel(logger, x, page, y) behave identically — and the Robot side
sees the same keyword either way, taking x and y. Put them wherever reads
best.
These names are reserved: a keyword cannot take an argument called page from
Robot Framework, because that name is spoken for. self is not usable either.
One further name is special without being filled in. A parameter called args
makes the keyword variadic — it receives Robot Framework's *args, so it
carries values to your function rather than from the library. It cannot have a
default, and anything you declare after it becomes a named-only argument on the
Robot side, so it reads best last.
Whatever you return is JSON-serialised on the way back, so it must be
serialisable — undefined arrives in Robot Framework as ${None}.
jsextension takes a single path, a comma-separated list, or a real list of
paths. Load it at import:
Custom selector strategies
A module can also register a selector engine, which then works anywhere a selector is accepted:
Debugging
You can attach a Node debugger to the Playwright process — to your own extension or to the library's internals.
1. Start Node with debugging enabled
Set ROBOT_FRAMEWORK_BROWSER_NODE_DEBUG_OPTIONS; its value is passed to the
Node process as CLI options.
| Option | Behaviour |
|---|---|
Bind it to a specific port so it does not collide with anything else:
Set it for the debug session rather than globally. For RobotCode, in
launch.json:
2. Attach
3. Run
- Set breakpoints in your extension — or in
site-packages/Browser/wrapper/index.jsto debug the library itself. - Set a breakpoint in the Robot code early, after
Library Browseris loaded and before your JS runs. This buys you the time to attach. - Start the Robot debug session and wait for that breakpoint.
- Switch to Run and Debug, choose Attach to Node, and start it.
Step 2 is the one people skip, and then wonder why the JS breakpoint never fires: without it the extension has already run by the time you attach.
Which extension point?
| You want | Use |
|---|---|
Before you write one
Somebody may have already written it. robotframework-browser-extensions collects working community extensions — accessibility checks with axe-core, visual comparison, network throttling, request mocking, element highlighting, and native Playwright page methods.
Reading one is also the fastest way to see the shape of a finished extension rather than a snippet. And when yours works, it belongs there too: it is a monorepo that takes pull requests, so publishing is one PR rather than a release process.