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. - Seven 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 seven names Browser fills in
Name a parameter one of these and the library passes the value 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 |
adoptContext | A function that hands a context you created to Browser — see Handing over a context you created |
defaultTimeout | The library timeout in milliseconds, as Set Browser Timeout sets it |
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.
New in Browser 20.4.0 Only page, context and browser need an open
browser. A keyword that takes one of them fails with No Browser is open but needed for this operation. when none is open. A keyword that takes none of them
runs without one, which is what lets a module
create the first browser itself.
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:
Handing over a context you created
New in Browser 20.7.0 A module can create a browser context itself and hand
it to Browser with adoptContext. That is how a library supports a platform
that Browser does not launch, such as an Electron application, without Browser
knowing about it:
The examples on this page are kept short. A real module closes the application
when anything after the launch fails, for example with try/catch around
firstWindow and adoptContext, because Browser cannot close a process it has
not taken over.
adoptContext(context, options) adds the context as a new browser and makes it
the active one, the same way Browser keeps a persistent context:
- Its pages become Browser's pages. The pages the context already has are
indexed and the first one becomes the active page, so
Click,Get Textand the other keywords work on it right away. Pages it opens later can be selected withSwitch Page. activePagepicks another page. Pass a page of the context to make it the active page instead of the first one, for example the window of an Electron application when Playwright saw one of its views first. A page of another context makesadoptContextfail without adopting anything.- It gets the library timeout, as the contexts Browser creates have, so the
function does not call
setDefaultTimeoutitself. optionsdescribe the browser. All of them are optional.nameis whatGet Browser Catalogreports as its type,adoptedby default, andheadlesssays whether it runs headless,falseby default.tracingrecords a trace. Pass the absolute path of a.ziptrace file or of a folder, which getstrace_<context id>.zip, and Browser starts tracing the context and saves the trace before it closes the context, as withtracingofNew Context. A relative path would be relative to Browser's Node process, not to the output directory. A library written in Python gets the path from Browser, see From a library written in Python.contextOptionssay how the context was created. Pass the options you created it with, in the form of Playwright's browser context options. Browser keywords that depend on them then work on it:DownloadneedsacceptDownloads, which an Electron application has by default, so pass{ acceptDownloads: true }for one.- Browser registers it like a context of
New Context. As soon asadoptContextresolves, the keyword calls become groups in the trace, if it is traced, and the log embeds the video of the active page, if it records one, in thesizeofrecordVideoincontextOptions, else of theirviewport, else 1280×720. - It returns the ids of the new browser and context, as an object with
browserIdandcontextId, pluspageIdif the context has a page, andvideoPath, the video of the active page ornull. Return it from your function and the keyword returns it to Robot Framework. - Closing the browser closes the context.
Close Browserand automatic closing treat it like any other browser. onClosereleases the rest. The async optiononCloseruns once after the context is closed, even when closing it failed. Pass it for what the context does not own. An Electron application quits with its context and needs none; a connection to an Android device does not close by itself:adoptContext(context, { onClose: () => device.close() }).
From a library written in Python
A library built on Browser in Python calls the module with call_js_keyword
and lets Browser decide where traces, videos and HAR files go, by the same rules
as for New Context. Type the keyword's arguments with Browser's types, as the
example does, so that Robot Framework converts False or a dictionary as it does
for New Context:
adopted_context_optionsreturns anAdoptedContextOptions, a typed dictionary withtracing,recordVideoandrecordHarasNew Contextwould use them,Nonewhen off:tracing=TrueandROBOT_FRAMEWORK_BROWSER_TRACINGgive Browser's trace folder, a video goes to Browser's video folder, and relative paths are resolved as forNew Context. Its arguments are converted like those ofNew Context, so an unknown key fails. An emptyrecordVideorecords with these defaults. Without asize, the video has the size of theviewportargument, the viewport you create the context with, or 1280×720.tracesDiris a new folder for Playwright's temporary trace files, as Browser gives one to each browser it launches. Name the function's parameters like the keys and**optionspasses them on. GivetracingtoadoptContext,tracesDirto the launch, and the others to Playwright when you create the context.- Nothing to register afterwards.
call_js_keywordregisters the context while the call runs, as soon asadoptContextresolves, so the keyword calls become groups in the trace and the log embeds the video, as described above. The keyword returns what the function returned, here the ids andvideoPath. defaultTimeoutis filled in bycall_js_keywordtoo, with the library timeout, andadoptContextgives it to the context. Pass your own only when your library keeps a timeout of its own; it is the one filled-in name you can override.
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 |
|---|---|
--inspect | Debugging on, process starts immediately. Attach whenever. |
--inspect-brk | Process pauses until a debugger attaches. Needed to debug startup. |
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?
Three shapes, not two. A Python plugin and a JavaScript module both extend the Browser instance Robot Framework loaded, from the inside. The third is not an extension point at all: your own Robot Framework library, which either sits alongside Browser or owns the instance itself.
| You want | Use |
|---|---|
| Playwright APIs Browser does not expose | JavaScript module |
| Assertion arguments like the built-in getters | Python plugin with AssertionEngine |
| To replace an existing keyword | Python plugin |
| A custom selector engine | JavaScript module |
| A platform Browser does not launch, such as Electron | JavaScript module that hands over its context, usually in a library of its own |
| Both Python and page-side logic in one keyword | Python plugin calling a JS module |
Business logic in Python — IF, TRY, parsing — with Browser unchanged | Your own Python library |
| Your own keywords and your own failure handling, with Browser underneath | Browser as a base |
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.