BROWSER

Extending Browser

Browser as a base

Own the Browser instance in your own library — what you get free, what one line restores, and what you build yourself.

On the previous page Robot Framework imports Browser and your library sits alongside it. This page is the other arrangement: Robot Framework imports only your library, and your library constructs Browser.

People arrive here when they are replacing the Browser import and a pile of resource-file keywords with a library of their own — a domain vocabulary rather than a browser vocabulary, with failure handling they control.

One question decides everything

Not how advanced your library is. Who imports Browser.

Alongside BrowserBrowser as a base
Robot Framework importsBrowser and your libraryonly your library
Who constructs Browser()Robot Frameworkyour library
How your library reaches itBuiltIn().get_library_instance("Browser")it holds the instance it made
Browser's listener is registeredautomaticallyonly if you register it

Everything below follows from owning the instance, and it is not a list of losses. It is three tiers.

TierWhat is in it
Free — because Robot Framework is runningargument conversion, outputdir, the validate and then operators
One line — register Browser as a listenerautomatic closing, scope settings, the Robot Framework context on the Node side
Yours to buildrun_on_failure, and a little of the tracing

Free — because Robot Framework is running

These hang off a Robot Framework run existing at all, not off who imported Browser. They are true in this arrangement whether or not you go on to register the listener.

New in Browser 20.4.0 Arguments convert from plain Python values.browser.click("id=x", "middle"), "2 seconds" into a timedelta, "validate" into an AssertionOperator — the same converters Robot Framework uses, and a Python None stays None. The four rules are on the previous page and they do not change here.

browser.outputdir is Robot Framework's ${OUTPUTDIR}. Screenshots, videos, traces and the Playwright log land in the run's output directory, exactly as they do for any other suite.

The validate and then assertion operators execute. They need an execution context, and there is one.

That last pair is worth stating plainly because it has been published wrongly before: these are properties of a Robot Framework run, not of Browser having been imported by it.

One line — registering the listener

Everything in that middle tier is implemented in Browser's listener methods. Robot Framework only looks for ROBOT_LIBRARY_LISTENER on libraries it imported, so when your library owns the instance, nobody tells Browser that a test started or ended.

Hand it over in your constructor:

class MyLibraryB:    """Browser used as a base, with this library owning the instance."""    ROBOT_LIBRARY_SCOPE = "GLOBAL"    # Robot Framework resolves the listener API version of every listener    # separately. Browser declares version 2, but this library would default to    # version 3, which has different method signatures.    ROBOT_LISTENER_API_VERSION = 2    def __init__(self):        self._browser = Browser(enable_playwright_debug=True)        # Robot Framework accepts a list of listeners and calls Browser's        # listener methods itself. Without this line Browser gets no suite and        # test events, so automatic closing and scope settings do not work.        self.ROBOT_LIBRARY_LISTENER = [self, self._browser]
Python15 linesUTF-8

This is a supported public contract of Browser, not a workaround. Two Robot Framework behaviours make it work: ROBOT_LIBRARY_LISTENER accepts a list, so your library and Browser can both be listeners; and for a library listener Robot Framework also looks for method names with a leading underscore, which is how it finds Browser._start_suite.

That leading underscore is not a private-API marker. It is the convention that stops a listener method from also becoming a keyword — Robot Framework's library API skips names beginning with _, and the library-listener lookup puts the underscore back.

You can write ROBOT_LIBRARY_LISTENER = self._browser instead, and Browser still gets its events. The list form is what lets you be a listener as well.

What the line buys

The library's tests run the same suite against two libraries that differ only in whether they hand Browser that registration. The difference is measured, not argued:

After three tests that each open a pageWith the lineWithout it
Pages still open13
A scope=Test browser timeout, in the next testreverted to 10 secondsstill 3 seconds
Node-side log records carrying the Robot Framework test name730

So the line buys three things: pages and contexts opened in a test are closed at its end, a scoped setting such as set_browser_timeout(…, "Test") reverts when that test ends, and the Robot Framework suite and test names reach the Node side, where Playwright's own log and traces carry them.

Two traps in the registration

Declare ROBOT_LISTENER_API_VERSION = 2. Robot Framework resolves the listener API version per listener, not per registration. In [self, self._browser], Browser gets version 2 from its own class attribute while your library defaults to version 3 — a different set of method signatures. Copy a version-2 signature such as _end_keyword(self, name, attrs) into a library that has not declared the version and you get a TypeError at run time that does not say why.

Give your library ROBOT_LIBRARY_SCOPE = "GLOBAL". Browser is GLOBAL and keeps state at class level, shared by every instance in the process. If Robot Framework constructs your library once per suite or per test, each new Browser() you make joins the state the previous one left behind.

Yours to build

One code path is not recovered by the listener, in either arrangement: Browser.run_keyword, the dynamic library API method Robot Framework calls to execute a Browser keyword. Robot Framework calls it only on the library it imported itself, and registering a listener is not importing a library. In this arrangement it calls run_keyword on your library, never on Browser.

What lives in there:

  • run_on_failure — no screenshot on a failing Browser call, listener or not. This is the one that matters, and the recipes below are how you get it back.
  • Trace groups in the non-default TracingGroupMode.Browser, Browser's rewriting of error messages, and pause_on_failure.

Trace groups in the default TracingGroupMode.Full are not in that list. They are opened and closed by listener methods, and a library listener receives events for every keyword in the run, so registering the listener gets you a trace group per Robot Framework keyword.

Recovering run_on_failure yourself

Three shapes, and no single correct one. Pick by the size of the library.

A decorator, best for a small library. It wraps the keywords you choose, takes the screenshot and re-raises. It is the one shape the library's tests cover, and it is written out in full on the previous page.

Your own _end_keyword, nearly free here — your library is already a listener, because that is how you registered Browser. Take a screenshot when attrs["status"] == "FAIL". Two things to know:

  • A library listener is told about every keyword in the run, not only its own library's. Browser's own listener has to check the owning library name to tell its keywords from everybody else's, and so will yours.
  • _end_keyword(self, name, attrs) is a listener API version 2 signature, so this is the same ROBOT_LISTENER_API_VERSION trap as above.

The granularity is different from the decorator's: a listener sees your keyword fail, not the individual Browser call inside it. That is usually what you want in a report.

A dynamic library, optionally through PythonLibCore, for a large one. Implement run_keyword yourself and put the failure handling in it: every keyword is covered by construction and nothing has to be remembered per keyword. Browser's own Browser.run_keyword is the worked reference — try, delegate, and on failure call the failure keyword before re-raising.

The log gets shallower here too

A Browser keyword called from Python is not a keyword in output.xml — Robot Framework never saw a keyword call — so everything Browser logs appears inside the keyword of your library that made the call. That is true of both arrangements, and here it is true of every Browser call you make, because they all come from your library.

The detailed Robot Framework log, one row per Browser keyword, becomes your own keyword's log. Log what a reader of the report needs, because the automatic detail is gone.

Cleaning up

Nothing leaks. Browser registers an atexit hook when it starts the Node process, so the process tree goes down when the interpreter exits whether or not a listener is registered. A suite teardown that closes the browser is not required for cleanliness — it is worth having only for the case where something kills the run with a signal and atexit never runs.

Where these examples come from

Every labelled block on this page is quoted from a real file, committed in this repository under examples/python-extension/, and a test fails the build if a quote stops matching it. MyLibraryB.py is a complete worked library for this page and context_b.robot is the suite that exercises it. MyLibraryB_no_listener.py is the same library without that registration, and the difference between those two runs is the table above.

Those files are a copy. They run as acceptance tests in the library itself, in atest/test/13_Python_Extension/, which is where a change to them belongs.