Using invisible_playwright with Robot Framework Browser

The cleanest fit of any framework here. Browser’s New Browser keyword takes both halves as documented named arguments, so there is no subclassing, no provider class and no configuration dictionary to reverse-engineer.

Verified against MarketSquare/robotframework-browser on main (Browser/keywords/playwright_state.py):

def new_browser(
    browser: SupportedBrowsers = SupportedBrowsers.chromium,
    ...
    executablePath: str | None = None,
    firefoxUserPrefs: dict[str, str | int | float | bool] | None = None,
)

Written against invisible-playwright 0.4.6.

The two values

The engine and the identity come from the Python package, which suits Robot Framework since it is Python underneath anyway.

pip install robotframework-browser invisible-playwright
rfbrowser init
invisible-playwright fetch
invisible-playwright path
python -c "import json;from invisible_playwright import get_default_stealth_prefs;print(json.dumps(get_default_stealth_prefs(seed=1, humanize=True)))" > prefs.json

Regenerate prefs.json when you upgrade the package. Keep the seed written next to it: that number is the identity, and losing it means losing the ability to reproduce a run.

The suite

*** Settings ***
Library     Browser

*** Variables ***
${BINARY}       /absolute/path/from/invisible-playwright path

*** Keywords ***
Open Stealth Browser
    ${prefs}=    Evaluate    json.load(open('prefs.json'))    modules=json
    New Browser
    ...    firefox
    ...    headless=True
    ...    executablePath=${BINARY}
    ...    firefoxUserPrefs=${prefs}

*** Test Cases ***
The page loads as a real browser
    Open Stealth Browser
    New Page    https://example.com
    Get Title    ==    Example Domain

firefox as the first argument is not optional: New Browser defaults to Chromium, and pointing executablePath at a Firefox build while the keyword still thinks it is launching Chromium fails in a way that is confusing to read.

Reading the prefs without a file

If you would rather not ship prefs.json alongside the suite, generate them inline. Robot Framework can call Python directly:

*** Keywords ***
Open Stealth Browser
    ${prefs}=    Evaluate
    ...    __import__('invisible_playwright').get_default_stealth_prefs(seed=1, humanize=True)
    ${binary}=    Evaluate    str(__import__('invisible_playwright').ensure_binary())
    New Browser    firefox    headless=True    executablePath=${binary}    firefoxUserPrefs=${prefs}

This has the advantage that the prefs can never drift from the installed package, which is the failure mode a checked-in JSON file eventually produces. The cost is that ensure_binary() may download on the first run of a suite, so a CI job with a short timeout wants the invisible-playwright fetch step separately.

Three things to get right

Do not pass userAgent. New Browser and New Context both accept one, and it is exactly the value that must not be set by hand: it comes from the engine’s real version and from the same seeded profile as everything else.

Be careful with New Context overrides. timezoneId, locale, viewport and colorScheme are all available there, and each one you set by hand is a value that now has to agree with the profile instead of being derived from it. If you are using a proxy, setting timezoneId to your own machine’s zone is the classic way to break a session that was otherwise fine.

One seed per identity, not per suite run. A fixed seed makes a failing test reproducible: you can separate the site changing from the browser changing. If you need several identities in one suite, open several browsers with different seeds rather than rerolling.

What this route gives up

Same as every route that is not the Python wrapper, and worth stating plainly:

  • Humanised pointer motion. The preference switches it on, but the paths are drawn from the seed by the Python driver. Through Robot Framework the pointer still teleports between coordinates.
  • Timezone and locale from the proxy exit. The wrapper resolves those at launch against the IP you are actually leaving from. The prefs carry whatever they were generated with, and nothing updates them at launch.

For a test suite this is usually the right trade, because a test suite is normally not going through a residential proxy and does not need the pointer to be convincing. It is the wrong trade if the suite is pointed at something that scores behaviour.

The other Robot library: SeleniumLibrary

If your suite is on SeleniumLibrary and not Browser, both halves are still reachable, through two different string-format arguments instead of one keyword:

*** Test Cases ***
Open the patched Firefox
    Open Browser    about:blank    Firefox
    ...    options=binary_location=r"/absolute/path/from/invisible-playwright path"
    ...    ff_profile_dir=set_preference("privacy.resistFingerprinting", False)

options= maps onto Selenium’s FirefoxOptions, so binary_location points at the engine. ff_profile_dir= accepts a chain of set_preference("key", "value") calls, which is where the profile goes.

The awkward part is real: there are several hundred preferences in a generated profile and this argument takes them as a semicolon-separated string of calls, so generate it rather than typing it:

from invisible_playwright import get_default_stealth_prefs

prefs = get_default_stealth_prefs(seed=1, humanize=True)
print(';'.join(f'set_preference("{k}", {v!r})' for k, v in prefs.items()))

Browser is the better library for this and for most things. This section exists because a suite that already uses SeleniumLibrary is not going to be rewritten for a browser engine, and it should still be able to use one.

Checking it worked

*** Test Cases ***
The engine is the patched one
    Open Stealth Browser
    New Page    https://example.com
    ${ua}=    Evaluate JavaScript    ${None}    () => navigator.userAgent
    Should Contain    ${ua}    Firefox
    ${gpu}=    Evaluate JavaScript    ${None}
    ...    () => { const g = document.createElement('canvas').getContext('webgl'); const d = g.getExtension('WEBGL_debug_renderer_info'); return g.getParameter(d.UNMASKED_RENDERER_WEBGL); }
    Should Not Contain    ${gpu}    Basic Render

The second assertion is the one people skip and should not: a basic or software renderer means the machine has no GPU, and that is a stronger signal than anything the automation itself leaks.

Short answers to the questions that lead here

Does Robot Framework Browser support a custom browser binary? Yes. New Browser takes executablePath and firefoxUserPrefs as documented named arguments, which makes it the cleanest fit of any framework here.

Do I need to say firefox? Yes, as the first argument. New Browser defaults to Chromium, and pointing executablePath at a Firefox build while the keyword still thinks it is launching Chromium fails in a way that is hard to read.

Can I do this with SeleniumLibrary instead? Yes, through options=binary_location and ff_profile_dir=set_preference(...). Generate that string rather than typing it: a profile carries several hundred preferences.

How do I load the preferences without shipping a JSON file? Call the Python function from Evaluate. The prefs then cannot drift from the installed package, which is the failure mode a checked-in file eventually produces.

Why should I not set timezoneId in New Context? Because it then has to agree with the address you are leaving from. Setting it to your own machine’s zone is the classic way to break a session that was otherwise fine.


Back to top

MIT licensed. Every page here is written against the current source of the thing it describes, and several record something we got wrong first.

This site uses Just the Docs, a documentation theme for Jekyll.