GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • BROWSER
  • Why Browser ▾
  • vs CypressSame test, side by side
  • vs Playwright TestSame test, side by side
  • vs SeleniumLibrarySame test, side by side
  • Docs ▾
  • How Browser worksPython, Node and Playwright
  • Running itThe Node process and its environment
  • Keywords
  • Releases
  • Community
  • Installation
  • Browser, context and page
  • Finding elements

This view

  • Original page
  • New address

GeezerVison is showing robotframework-browser.org — Python plugins — Robot Framework Browser

The Python plugin API adds keywords to Browser, or replaces existing ones, without forking the library. It is provided by PythonLibCore.

Note

This is the more capable of the two extension points. A JavaScript module runs only on the Node side; a Python plugin can use both, and can use AssertionEngine directly — decorate the keyword with Browser.assertion_engine.with_assertion_polling and call verify_assertion yourself — to give your keywords the same retrying assertion arguments the built-in getters have.

A Python-only plugin

Subclass LibraryComponent and decorate with @keyword:

import jsonfrom robot.api import loggerfrom robot.api.deco import keywordfrom Browser.base.librarycomponent import LibraryComponentfrom Browser.generated.playwright_pb2 import Requestclass SimplePythonPlugin(LibraryComponent):    @keyword    def cookie_via_public_api(self) -> dict:        """Uses Browser's own public API."""        cookies = self.library.get_cookies()        logger.debug(json.dumps(cookies, indent=4, default=str))        assert len(cookies) == 1, "Too many cookies."        return {"name": cookies[0]["name"], "value": cookies[0]["value"]}    @keyword    def cookie_via_grpc(self) -> dict:        """Calls the Node side directly over gRPC."""        with self.playwright.grpc_channel() as stub:            response = stub.GetCookies(Request().Empty())            cookies = json.loads(response.json)        return {"name": cookies[0]["name"], "value": cookies[0]["value"]}

Load it at import:

*** Settings ***Library    Browser    plugins=${CURDIR}/SimplePythonPlugin.py*** Test Cases ***Read A Cookie    New Page    https://example.com    Add Cookie    session    abc123    url=https://example.com    ${cookie} =    Cookie Via Public Api    Should Be Equal    ${cookie}[name]    session

Two routes are shown above deliberately. self.library is the supported public API and should be your default. Dropping to gRPC gets you at anything the Node side can do, at the cost of coupling to internals that may change.

Aside

When self.library.new_page(...) fails, the failed page stays open and active until your plugin keyword returns, so the failure screenshot shows the page that failed. Let the error propagate and there is nothing more to do. If you catch it and carry on, the rest of the keyword runs against the failed page unless you switch back yourself:

previous = self.library.switch_page("CURRENT")  # id of the active pagetry:    self.library.new_page(url)except Exception:    self.library.switch_page(previous)

Several plugins can be loaded at once — plugins= takes one name, a comma-separated list, or a real list. Arguments go after the class, separated by semicolons: plugins=pkg.Plugin;arg1;kw=val.

Two rules that fail loudly, and one that does not:

  • The class name must match the module or file name, or the import fails with DataError.
  • A plugin that does not inherit LibraryComponent fails with PluginError.
  • A plugin keyword whose method name matches a built-in silently replaces it. Plugins load last — library keywords, then JavaScript extensions, then plugins — so last wins, with no warning. The exception is the handful of keywords declared with an explicit @keyword(name=...), such as Evaluate JavaScript, Get BoundingBox and the storage keywords: matching their method name creates a duplicate instead, and the suite fails with Keyword with same name defined multiple times. Give your method the same @keyword(name=...) to replace one of those. Every plugin keyword is tagged Plugin, which is how you spot one in Libdoc.

Selectors inside a plugin

The public keywords resolve the Set Selector Prefix value for you, so a plugin that calls them needs to do nothing:

@keyworddef disable_element(self, selector):    """Disables an element."""    self.library.evaluate_javascript(selector, "e => e.disabled = true")

Resolve it yourself with self.resolve_selector(selector) only when you bypass the public API — a direct gRPC call, or call_js_keyword.

For a keyword that should also honour presenter mode — highlighting the element and pausing so a human can follow along — use presenter_mode, which resolves the selector as well:

@keyworddef highlight_and_blur(self, selector):    """Blurs the element, honouring presenter mode."""    selector = self.presenter_mode(selector, self.strict_mode)    self.call_js_keyword("myBlur", selector=selector)

call_js_keyword reaches a keyword from any JavaScript module registered on the Node side — one this plugin loaded with initialize_js_extension, or one loaded through the library's jsextension= argument — it is not a way to call arbitrary Playwright methods, so myBlur has to exist in that module.

Calling JavaScript from a Python plugin

Load a JS module in the constructor, then call into it:

from pathlib import Pathfrom robot.api.deco import keywordfrom Browser import Browserfrom Browser.base.librarycomponent import LibraryComponentclass PythonPlugin(LibraryComponent):    def __init__(self, library: Browser):        super().__init__(library)        self.initialize_js_extension(Path(__file__).parent.resolve() / "JSPlugin.js")    @keyword    def my_mouse_wheel(self, x: int, y: int):        """Calls a custom JavaScript keyword from JSPlugin.js."""        return self.call_js_keyword("myMouseWheel", x=x, y=y)

Watch out

Every argument to call_js_keyword must be named and JSON serialisable — passing one positionally raises TypeError. The Node side fills in page, context, browser, logger, playwright and adoptContext itself, so a value you pass for them is ignored. defaultTimeout is the library timeout in milliseconds unless you pass your own.

This combination is usually the one you want: the keyword signature, type conversion and documentation are Python, and only the part that genuinely needs the page is JavaScript.

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.