# Robot Framework Browser > A browser automation library for Robot Framework, powered by Playwright. Speed, reliability and visibility, with assertions built into the keywords. Source: [https://robotframework-browser.org/](https://robotframework-browser.org/) Robot Framework deserves a browser automation solution designed for the 2020s. Browser library, powered by [Playwright](https://playwright.dev/), provides speed, reliability and visibility. - [Get started](https://robotframework-browser.org/#start) - [Why Browser](https://robotframework-browser.org/why) - Browser: **21.0.0** - Playwright: **1.64.1** - Node: **26.13.0** - Keywords: **154** - Stars: **661** - Contributors: **211** - Releases: **192** - Sleeps: **0** ## Speed, reliability and visibility. **Speed** One Node process drives Chromium, Firefox and WebKit. No driver binary per browser, no version to keep in step with an update. **Reliability** Every action waits for the element to be actionable before it acts. No sleeps to tune, and no flake to chase down. **Visibility** Video, a Playwright trace and a screenshot on failure, attached to the Robot Framework log. - Conscientious assertions - Precise and fast browser window and tab control - Chainable selector strategies - Good shadow DOM support - Simple descriptors for mobile devices - Sending HTTP requests [How it works, and how it compares](https://robotframework-browser.org/why) ## Running in five minutes. Three blocks. Copy them in order and you have a passing test — nothing to configure in between. ### Install the library and the browsers No Node.js needed — the `[bb]` extra brings one along in the wheel. `rfbrowser install` downloads the browser binaries Playwright drives. The tabs below pick your shell automatically. ```bash $ pip install "robotframework-browser[bb]" Successfully installed robotframework-browser-21.0.0 robotframework-browser-batteries-21.0.0 $ rfbrowser install Installing Playwright browser binaries… ✓ chromium downloaded ✓ firefox downloaded ✓ webkit downloaded ``` ```powershell PS> py -m pip install "robotframework-browser[bb]" Successfully installed robotframework-browser-21.0.0 robotframework-browser-batteries-21.0.0 PS> py -m Browser.entry install Installing Playwright browser binaries… ✓ chromium downloaded ✓ firefox downloaded ✓ webkit downloaded ``` Want a Node toolchain of your own instead, or on a platform with no wheel? That route is two different commands and [Installation](https://robotframework-browser.org/docs/start/installation) has both. ### Write a test One import, no setup keyword, no explicit waits. `Get Text` both reads the value and asserts it — and it keeps re-reading until the search results arrive, which is why nothing here waits for them. ```robot [first.robot] *** Settings *** Library Browser *** Test Cases *** Search The Syllabus New Page https://syllabus.robotframework.org Fill Text id=search_input_react Argument Types Get Text body *= 2.5.2.8 ``` ### Run it Robot Framework as usual — the browser opens, acts and closes. ```bash $ robot first.robot ========================================================= First ✓ Search Robot Framework 1 test, 1 passed, 0 failed ``` Next: the [getting-started guide](https://robotframework-browser.org/docs/start/getting-started), or the [keyword reference](https://robotframework-browser.org/keywords). Writing your tests with an AI assistant? This site is also published for language models: [llms.txt](https://robotframework-browser.org/llms.txt) lists every page as Markdown, [llms-full.txt](https://robotframework-browser.org/llms-full.txt) holds all the docs in one file, and [keywords.md](https://robotframework-browser.org/keywords.md) the complete keyword reference. Point your assistant there. ## Use. Benefit. Contribute. Browser library is built in the open by the Robot Framework community — 206 people so far. Questions, ideas and bug reports are how most of them started, and how the library got this far. - [Community and contributing](https://robotframework-browser.org/community) - [Forum](https://forum.robotframework.org/c/libraries/browser) - [GitHub](https://github.com/MarketSquare/robotframework-browser) ### And 206 people who made it better Bug reports, ideas, documentation, running main against a real suite before it was ever released. Most of them never wrote a line of code for Browser. [See who did what](https://robotframework-browser.org/community#hall). **Let's make the best Browser library.** # Installation > The two ways to install Browser — with a bundled Node.js runtime, or with one you provide yourself — and how to upgrade and remove each. Source: [https://robotframework-browser.org/docs/start/installation](https://robotframework-browser.org/docs/start/installation) Browser is a Python library that drives Playwright running in Node.js, so something has to supply that Node.js. There are two ways to do it, and the only real decision at install time is which. Both need **Python 3.10 or newer**, and both should be done in a virtual environment. ## Choose a route | | Without Node.js | With Node.js | | --- | --- | --- | | `pip install` | `robotframework-browser[bb]` | `robotframework-browser` | | Then run | `rfbrowser install` | `rfbrowser init` | | Node.js from | The wheel, already on disk after `pip` | You, on the `PATH` | | Works on | Linux x64/arm64, Windows x64, macOS x64/arm64 | Anywhere Python and Node.js both run | | Extra npm packages | No — what ships is what you get | Yes | | Leaves you `npm` and `npx` | No | Yes | Start on the left. It is fewer moving parts, and in an environment where installing Node.js means a ticket and a wait, it removes the whole conversation. Go right when something in that column is yours: a platform with no wheel, a JavaScript extension that needs npm packages Browser does not ship, or an existing Node toolchain you would rather Browser used. The two are not exclusive and not a one-way door. They differ only in where the Node.js comes from — the keywords, the arguments and the behaviour are the same either way — so switching later costs an uninstall and a reinstall, not a rewrite. ## Without Node.js ### Install ```bash $ pip install "robotframework-browser[bb]" Successfully installed robotframework-browser-21.0.0 robotframework-browser-batteries-21.0.0 ``` ```powershell PS> py -m pip install "robotframework-browser[bb]" Successfully installed robotframework-browser-21.0.0 robotframework-browser-batteries-21.0.0 ``` The `[bb]` extra pulls in [robotframework-browser-batteries](https://pypi.org/project/robotframework-browser-batteries/), a platform-specific wheel carrying an unmodified official Node.js build, the gRPC wrapper and its production Node dependencies. It is not a single packed binary: it is a real `node` and a real `node_modules`, installed into `site-packages/BrowserBatteries/bin/`. Which Node.js that is moves with the library, so it is a fact about a release rather than about the package. The [release notes](https://robotframework-browser.org/releases) say which Node.js the BrowserBatteries wheel was built with — check there for the release you are installing. The quotes are for zsh, which is the default shell on macOS and treats bare square brackets as a glob. `bash` and PowerShell do not need them and do not mind them. > **Aside:** > > Installing `robotframework-browser-batteries` by name gets you the same two > packages — it depends on the matching `robotframework-browser` — so an existing > requirements file spelling it that way is not wrong. `robotframework-browser[bb]` > is written from the side you are more likely to remember: the library, with > batteries. The two packages are **tied to one version** and having them differ is not supported. That is what the extra buys beyond a shorter line: pip resolves both from a single constraint, so they cannot drift apart. ### Where there is a wheel A wheel is published for each of these, and tagged so that pip declines to install it where the bundled Node.js could not start: | Platform | Architecture | Needs at least | | --- | --- | --- | | Linux | x64 | glibc 2.28 — Debian 10, RHEL 8, Ubuntu 20.04 | | Linux | arm64 | glibc 2.28 — Debian 10, RHEL 8, Ubuntu 20.04 | | Windows | x64 | Windows 10 or Windows Server 2016 | | macOS | x64 | macOS 13.5 | | macOS | arm64 | macOS 13.5 | There is no musl wheel, and none for Windows on arm. Anywhere else, pip reports that it found no matching distribution — which is the wheel tags doing their job, and the signal to use the other route. > **Watch out:** > > macOS **13.0 to 13.4** is the one case the tags cannot catch. Node.js needs > 13.5, but pip only ever generates `macosx_13_0_*` tags, so the wheel installs > and then the bundled Node.js fails to start. On those four versions, either > update macOS or install Node.js yourself. ### Browser binaries Playwright's browser builds are downloaded separately, because they are not in the wheel: ```bash $ rfbrowser install Installing Playwright browser binaries… ✓ chromium downloaded ✓ firefox downloaded ✓ webkit downloaded ``` ```powershell PS> py -m Browser.entry install Installing Playwright browser binaries… ✓ chromium downloaded ✓ firefox downloaded ✓ webkit downloaded ``` Three engines, a few hundred megabytes, once per environment. Name one to get only that one — `rfbrowser install firefox`. > **Watch out:** > > **rfbrowser init** **is not for this route.** It runs `npm ci` and stops with > *Couldn't execute npm. Please ensure you have node.js and npm installed and in > PATH* — which reads like a broken installation and is not one. On this route the > command is `rfbrowser install`. You can also skip this step entirely. If you already have a Chromium-based browser — Chrome or Edge — you can drive that instead by passing a `channel` to `New Browser`, and download nothing: ```robot-repl New Browser chromium channel=chrome ``` That combination — wheels only, no binary download — is what makes this route work on a machine with no route to the public internet at all. ### Verify ```bash $ rfbrowser --version Installed Browser library version is: "21.0.0" Installed Robot Framework version: "7.4.1" Required Playwright is: "1.64.1" ``` It reports the Playwright as *Required* rather than *Installed* here, because reading the installed version means asking `npm`, and there is no `npm` on this route. The number is the one that ships in the wheel either way. Note what it does **not** report: the browser binaries. `rfbrowser --version` staying green says nothing about whether `rfbrowser install` ever ran. ### Upgrading Upgrade both packages together, then rebuild the Node side: ```bash $ pip install -U robotframework-browser robotframework-browser-batteries $ rfbrowser clean-node Delete library node dependencies from …/BrowserBatteries/bin/wrapper/node_modules $ rfbrowser install ``` `clean-node` removes the old Node dependencies and browser binaries. Skipping it leaves the previous release's binaries behind, and a Playwright that has moved on does not necessarily drive them. ### Uninstalling ```bash $ rfbrowser clean-node $ pip uninstall robotframework-browser robotframework-browser-batteries ``` `clean-node` first, and in that order: it is what deletes the browser binaries, and once pip has removed the package there is nothing left that knows where they were. ## With Node.js ### Install Install [Node.js](https://nodejs.org/en/download/) first — at the time of writing that is **22, 24 or 26**, and each release states the lines it supports in its [release notes](https://robotframework-browser.org/releases). Make sure `npm` is on your `PATH`. Then: ```bash $ pip install robotframework-browser Successfully installed robotframework-browser-21.0.0 ``` ```powershell PS> py -m pip install robotframework-browser Successfully installed robotframework-browser-21.0.0 ``` ### Node dependencies and browser binaries One command does both, and the library does not work until it has run: ```bash $ rfbrowser init Installing node dependencies... Installing browser binaries to 0 rfbrowser init completed ``` ```powershell PS> py -m Browser.entry init Installing node dependencies... Installing browser binaries to 0 rfbrowser init completed ``` `init` runs `npm ci --omit=dev` against the lockfile shipped in the package, then downloads the browsers. Name engines to narrow the download — `rfbrowser init chromium firefox` — or pass `--skip-browsers` to install the Node dependencies alone and take responsibility for the binaries yourself. > **Watch out:** > > **rfbrowser install** **is not for this route.** It exists for installations that > have BrowserBatteries, and running it here does not install the Node > dependencies `init` is responsible for. On this route the command is > `rfbrowser init`. Do not add npm packages to `Browser/wrapper` by hand either. `init` runs `npm ci` there, which deletes `node_modules` and reinstalls exactly the shipped production dependencies, so anything you put there disappears at the next init or upgrade. Extensions that need their own packages are covered in [JavaScript extensions](https://robotframework-browser.org/docs/extending/javascript-extensions). > **Note:** > > **On Node.js 26 with npm 12**, `init` needs two extra steps. npm 12 no longer > runs post-install scripts unless they are approved, and several of Browser's > dependencies use them. Run `rfbrowser init` once, then > `npm approve-scripts --allow-scripts-pending` to list what is waiting, then > `npm approve-scripts ` for the ones belonging to Browser — > review the list, because it covers every npm project on the machine, not only > this one — and run `rfbrowser init` again. > > This is a moving target and it is npm's, not ours. It does not arise without > Node.js, where the dependencies are already built into the wheel. ### Verify ```bash $ rfbrowser --version Installed Browser library version is: "21.0.0" Installed Robot Framework version: "7.4.1" Installed Playwright is: "1.64.1" ``` *Installed* rather than *Required* means the number came from `npm list` in the wrapper directory — so a line reading *Installed* is also evidence that `init` got as far as writing `node_modules`. It still says nothing about the browser binaries. ### Upgrading ```bash $ pip install -U robotframework-browser $ rfbrowser clean-node Delete library node dependencies from …/Browser/wrapper/node_modules $ rfbrowser init ``` `clean-node` between the two is not optional housekeeping. `npm ci` reinstalls from the new lockfile, but the browser binaries from the previous release are outside its reach and stay until something deletes them. ### Uninstalling ```bash $ rfbrowser clean-node $ pip uninstall robotframework-browser ``` ## Behind a proxy, or offline Both routes reach the network, but for different things, and that is the whole of what changes here. Without Node.js there is one download — the browser binaries — and it can be avoided altogether by driving an installed Chrome or Edge through `channel`. Wheels themselves can travel by hand: `pip download robotframework-browser-batteries` on a connected machine gets you the pair, and they install from a directory on the target with no index at all. With Node.js there are two: npm resolving the wrapper's dependencies, and Playwright fetching the binaries. Both have to work, and an internal npm registry usually has to be configured before the first `init`. The browser download itself is Playwright's, and so are the variables that steer it through a proxy, a custom certificate authority or an internal mirror. They reach Playwright unchanged from either route, and [Playwright documents them](https://playwright.dev/docs/browsers#install-behind-a-firewall-or-a-proxy) — we deliberately do not copy the list, because a copy is wrong the day it changes. ## Never install into the system Python > **Note:** > > Use a virtual environment — `uv`, `venv` or `pyenv`, whichever your team already > uses. Installing into the system Python needs root, writes browser binaries into > a directory you do not own, and is painful to unpick. You will also need to test > against a second Python version sooner than you expect. For Node.js on the second route, use a version manager for the same reason: `n` or `nvm` on Linux and macOS, `nvm-windows` or `nodist` on Windows. Where the files actually land, and what the two halves do at run time, is [how Browser works](https://robotframework-browser.org/docs/concepts/architecture). # Getting started > Install Browser, write a first test, and run it. Source: [https://robotframework-browser.org/docs/start/getting-started](https://robotframework-browser.org/docs/start/getting-started) Install the library and its browser binaries, write one test, run it. ## Before you start You need **Python 3.10 or newer**, and a virtual environment to put this in — `uv`, `venv` or `pyenv`, whichever your team uses. ## Install ```bash $ pip install "robotframework-browser[bb]" Successfully installed robotframework-browser-21.0.0 robotframework-browser-batteries-21.0.0 $ rfbrowser install Installing Playwright browser binaries… ✓ chromium downloaded ✓ firefox downloaded ✓ webkit downloaded ``` ```powershell PS> py -m pip install "robotframework-browser[bb]" Successfully installed robotframework-browser-21.0.0 robotframework-browser-batteries-21.0.0 PS> py -m Browser.entry install Installing Playwright browser binaries… ✓ chromium downloaded ✓ firefox downloaded ✓ webkit downloaded ``` That is the route that needs no Node.js: the `[bb]` extra brings a Node.js runtime along in the wheel. `rfbrowser install` then downloads three browser engines, so it is the slow part and it needs a few hundred megabytes — once per environment. If you would rather provide Node.js yourself — because your platform has no wheel, or because a JavaScript extension needs npm packages Browser does not ship — the commands are `pip install robotframework-browser` and `rfbrowser init` instead. Both routes, in full, are on [Installation](https://robotframework-browser.org/docs/start/installation). ## Your first test The assertion is part of the keyword — `Get Title` both reads and checks. The browser runs **headless** by default, so this prints a result without anything appearing on screen. To watch it, open the browser yourself above `New Page`: ```robot-repl New Browser chromium headless=False ``` ```robot [first.robot] *** Settings *** Library Browser *** Test Cases *** Open The Keyword Reference New Page https://robotframework-browser.org Get Title *= Robot Framework Browser Click nav[aria-label="Main"] >> text="Keywords" Get Title *= Keyword reference ``` It drives this site, so you can run it right now without an application of your own — and the pages it touches are the ones you are reading. Two things are doing work in that selector, and both are worth copying. `>>` chains: everything left of it narrows the search, so `text="Keywords"` is looked for only inside the main navigation. The page has a second link with that exact text — in the row of figures on the landing page — and Playwright is strict: a selector matching more than one element is an error, not a silent first-match. The quotes make it an *exact* match. Without them, `text=Keywords` is a case-insensitive substring, which would also find "keywords" inside a sentence. See [selectors](https://robotframework-browser.org/docs/concepts/selectors). ## Run it ```bash $ robot first.robot Open The Keyword Reference | PASS | 1 test, 1 passed, 0 failed ``` Robot Framework writes `log.html` into the directory you ran `robot` from — not next to the test file — unless you pass `--outputdir`. Open it — every keyword, its arguments and its result are in there, and it is the first place to look when something fails. ## What is not in that test No `Sleep`, and no wait keyword before anything. Two different mechanisms are doing that work: - **Action keywords** like `Click` wait for the element to be actionable — attached, visible, stable and able to receive the click — before acting. - **Assertions** like the `Get Title` line above re-read the value until it matches or the retry window expires. So a keyword with an assertion operator is itself the wait. A getter *without* one reads once and returns. That distinction is the whole of [assertions](https://robotframework-browser.org/docs/concepts/assertions). # Browser, context and page > The three layers Playwright is built on — and why a new context is the cheapest clean slate you will ever get. Source: [https://robotframework-browser.org/docs/concepts/browser-context-page](https://robotframework-browser.org/docs/concepts/browser-context-page) Browser works in three layers. Almost every question about isolation, speed, or "why is this test seeing the previous test's login" is answered by knowing which layer you are on. ```text Browser · chromium │ ├── Context 1 · alice │ ├── Page · tab │ └── Page · tab │ └── Context 2 · bob └── Page · tab ``` One browser process, two signed-in users, three tabs. The two contexts share nothing — separate cookies, separate storage, separate permissions — which is the whole reason the layers are worth learning. | Layer | Is | Costs | Opened with | | --- | --- | --- | --- | | **Browser** | A Chromium, Firefox or WebKit process | Slowest | `New Browser` | | **Context** | An isolated session inside it | Cheapest | `New Context` | | **Page** | A tab, with its own history | Cheap | `New Page` | ## The browser One process, one engine. Three are available, and between them they cover what people actually use: | Engine | Ships in | | --- | --- | | `chromium` | Google Chrome, Microsoft Edge, Opera | | `firefox` | Mozilla Firefox | | `webkit` | Safari on macOS and iOS | Playwright brings its own binaries, so there is no geckodriver, no chromedriver, and no driver version to keep in step with a browser version. The same three engines run on Windows, Linux and macOS. `New Browser` starts headless unless you say otherwise — `Open Browser`, being a debugging tool, starts headful: ```robot-repl New Browser chromium headless=False ``` The browser layer is where the *environment* is decided: which engine, headless or not, a proxy, a slow-motion delay, the browser's own timeout, extra launch arguments. ### Browsers are reused `New Browser` with the same arguments as an earlier `New Browser` call does not start a second process — it switches to that one. That is what makes it safe to call in a suite setup that runs many times. A browser that `New Page` or `New Context` created implicitly is not a reuse candidate, so an explicit `New Browser` after them does start a second process. To force a new one deliberately: ```robot-repl New Browser chromium reuse_existing=False ``` ## The context A context is an independent session inside an already-running browser: its own cookies, its own storage, its own permissions. Two contexts share nothing. The closest everyday equivalent is a fresh incognito window. This is the layer worth understanding, because it is where Browser is structurally faster than the older tools. With tools that give you one session per browser process, an isolated session costs a process start. Here it is one call inside a process that is already warm: ```robot-repl New Context # a clean slate, in a few milliseconds ``` So "log in as a different user" costs a few milliseconds of context creation rather than a browser start, and so does "start this test from a known-clean state". You do not clear cookies; you throw the context away and open another. The context is also where the interesting configuration lives: ```robot-repl New Context viewport={'width': 1920, 'height': 1080} New Context locale=de-DE timezoneId=Europe/Berlin New Context geolocation={'latitude': 48.86, 'longitude': 2.35} New Context httpCredentials={'username': '$user', 'password': '$pwd'} New Context acceptDownloads=True New Context colorScheme=dark ``` Downloads are accepted by default. Pass `acceptDownloads=False` if you want the browser to refuse them. The `$user` and `$pwd` above are not a typo. `httpCredentials` refuses a plain value — it takes the `$name` placeholder form, and resolves the names from variables, so the password never reaches the log. Tracing and video recording are context-level too, which is why a trace covers exactly one session: ```robot-repl New Context tracing=True New Context recordVideo={'dir': '${OUTPUT_DIR}/video'} ``` ### Logging in once, and reusing it A context starts empty: no cookies, no `localStorage`, nobody logged in. Doing the login again in every test is the slowest thing most suites do, and the least interesting to debug when it breaks. `Save Storage State` takes what the *active context* has accumulated and writes it to a file; `New Context` takes that file back: ```robot *** Test Cases *** Log In Once New Context New Page ${LOGIN_URL} Fill Secret id=username $USERNAME Fill Secret id=password $PASSWORD Click id=submit Get Text id=header == Signed in ${state} = Save Storage State # A second session, already authenticated. New Context storageState=${state} New Page ${APP_URL} Get Text id=header == Signed in ``` #### What actually travels Less than you might expect, and this is the part worth knowing before you rely on it. Playwright's snapshot *can* carry cookies, `localStorage`, IndexedDB and virtual WebAuthn credentials — but the last two only when asked for, and Browser does not ask. It calls `storageState` with a path and nothing else, so you get the defaults: | State | Restored | | | --- | --- | --- | | Cookies | **yes** | Session cookies included, subject to their own expiry | | `localStorage` | **yes** | Per origin, exactly as saved | | `sessionStorage` | no | By definition tied to one tab; it is not in the file at all | | IndexedDB | no | Playwright offers it as an option; Browser does not pass it | | WebAuthn credentials | no | Same reason | So a login that keeps its token in a cookie or in `localStorage` restores cleanly. One that keeps it in `sessionStorage` does not, and no amount of saving will change that — you have to log in per context. #### Three things that catch people **The file does not survive the run.** It is written to `${OUTPUT_DIR}/browser/state/`, and that whole directory is deleted at the start of every execution. Reuse within a run is what this is for; to carry a session between runs, copy the file somewhere the next run will not wipe. **The path must exist.** Anything else fails immediately and by name: ```text ValueError: storageState argument value 'does-not-exist.json' is not file, but it should be. ``` It is checked before the context is created, so a typo costs no browser time. **The file is a credential.** It contains live session cookies and whatever the app put in `localStorage`. Anyone holding it is logged in as that user. Keep it out of version control and out of build artefacts you publish. ### Persistent contexts `New Persistent Context` is the exception to the shape: it takes a user data directory and creates its own browser, so state *survives* between runs. Useful when a real profile is the thing under test, and the wrong tool for isolation — a browser opened this way cannot host additional contexts. ## The page A page is a tab. It holds the document and its own history, and it is where every selector resolves. ```robot *** Test Cases *** Starting A Browser With A Page New Browser chromium headless=False New Context viewport={'width': 1920, 'height': 1080} New Page https://robotframework-browser.org Get Title *= Robot Framework Browser ``` A popup, a target-blank link or a second tab is another page in the same context, so it shares the login. It does **not** become the active page on its own — reach it with `Switch Page`, given `NEW`, which returns the id of the page you came from: ```robot-repl Click text=Open report ${previous} = Switch Page NEW Get Title *= Report Switch Page ${previous} ``` ## You rarely open all three The layers fill themselves in downwards. `New Page` with nothing open starts a browser and a context first, using defaults: ```robot-repl New Page https://robotframework-browser.org ``` `Open Browser` does all three in one call. It is built for experiments and debugging; when you care about the context, open the three yourself. ## What is open right now Every browser, context and page has an id, and `Get Browser Catalog` returns the whole tree: ```text chromium browser=94c1… ├── context=7f2a… │ ├── page=3dce… /login │ └── page=8b17… /cart └── context=b3d9… └── page=1f60… /admin firefox browser=1ae8… └── context=5c04… └── page=42aa… about:blank ``` Drawn as a tree here for readability; the keyword returns a list of dictionaries, one per browser, each carrying its contexts and their pages. What the tree does not show is which branch the next keyword will act on. That lives in three fields, and they are not the same kind of thing. `activeBrowser` is a **boolean**, one per browser, and exactly one is true — here it is `chromium`. `activeContext` and `activePage` hold **ids**, and *every* browser and context carries them whether or not it is the active branch: `context=b3d9…` above is idle, and still names the page it would use if you switched to it. So in this catalog the next `Click` lands on `page=3dce…` at `/login`: chromium is the true `activeBrowser`, its `activeContext` is `context=7f2a…`, and that context's `activePage` is `page=3dce…`. Read the three in that order and you always know where you are. This is the fastest way to answer "what does the library think is running" when a suite has drifted from what you expected. `Get Browser Ids`, `Get Context Ids` and `Get Page Ids` return the pieces individually. Keywords act on the *active* browser, context and page unless you say otherwise, and `Switch Browser`, `Switch Context` and `Switch Page` move that pointer. ## When things close By default, the **contexts and pages** a test opened are closed when the test ends. Browsers are not: no auto-closing level closes a browser per test or per suite, so a browser lives until execution ends or you call `Close Browser`. The exceptions are a browser opened by `New Persistent Context` and one that a JavaScript extension [handed over with `adoptContext`](https://robotframework-browser.org/docs/extending/javascript-extensions#handing-over-a-context-you-created) — each is closed together with its context. That setting is `auto_closing_level`, and it has four values: | Level | Contexts and pages are closed | | --- | --- | | `TEST` | At the end of the test that created them (the default). Anything created in suite setup lives until suite teardown. | | `SUITE` | At the end of the suite that created them | | `MANUAL` | Never during the run — everything closes when execution ends | | `KEEP` | Never, not even at the end. The node process stays alive too. | ```robot *** Settings *** Library Browser auto_closing_level=SUITE ``` `KEEP` leaves processes running on purpose — it exists so you can poke at a page after a failing test while writing it. **Never set it in CI**, or runs will leak browser processes until the machine gives up. ## Which layer for which job | You want | Open | | --- | --- | | A second user session | A new **context** | | A popup or a second tab in the same session | A new **page** | | A clean slate between tests | A new **context** — not a new browser | | A different engine, or a GUI | A new **browser** | | A proxy | Either — `proxy` is an argument on **both** `New Browser` and `New Context` | | A profile that survives the run | `New Persistent Context` | | A mobile device profile | A new **context** from `Get Device` | The third row is the one that decides how long your suite takes. Reaching for a new browser where a new context would do is the single most common reason a Browser suite runs slower than it needs to. ## In short - Browser = process, context = session, page = tab. - Contexts are the cheap unit of isolation. Use them freely. - `New Page` opens whatever is missing underneath it. - Cookies, storage, permissions, viewport, locale, tracing and video all belong to the **context**. - `auto_closing_level=KEEP` is for writing tests, never for running them. # Finding elements > Which selector strategy to reach for, why the order matters, and how to chain across iframes and shadow DOM. Source: [https://robotframework-browser.org/docs/concepts/selectors](https://robotframework-browser.org/docs/concepts/selectors) Every keyword that touches the page takes a `selector`. Which strategy you choose decides how often your suite breaks for reasons that have nothing to do with the software under test. This page covers the strategies, the order I would reach for them in, and the syntax for chaining, iframes and shadow DOM. ## Pick a strategy > **Note:** > > **The ranking below is opinionated!** > > It optimises for one thing: *a selector that keeps working when the page is redesigned but the feature is unchanged.* > > Every project is different, and you should base your selector strategy on other criteria as well. For example, prior knowledge, GUI framework, or other factors. | | Strategy | Reach for it when | | --- | --- | --- | | 1 | role= | The element has a proper accessible role and name. This is the default choice. | | 2 | data-testid= | Stability matters more than testing the interface as a user meets it. | | 3 | text= | The visible text is the thing you actually mean, and the app is single-language. | | 4 | label= | A form field, or a widget with an `aria-label`, has no role and name to reach it by, but a label the user reads. | | 5 | id= | You know the id is contractual, not incidental. | | 6 | css= | None of the above identify the element. | | 7 | xpath= | Genuinely nothing else can select it. | ### Example: Our Landing Page On our landing page, we have some buttons and links. One of them is a colour theme toggle, which is a button with a visible label and aria-label. It is the only button with that aria-label, so it is a perfect candidate for a `role=` selector. However, it also has a `data-testid` attribute, which is a good candidate for a `data-testid=` selector. The visible text is "DARK", which is a good candidate for a `text=` selector. ![A toggle button labelled DARK in the top-right corner](https://robotframework-browser.org/images/color-toggle.png) Color toggle at top right See the following HTML DOM snippet for the button: ```html [Color Theme Toggle] ``` ### 1. `role=` — how the user finds it ```robot-repl Click role=button[name="DARK-Mode colour theme. Activate to change."] # ^ matches the aria-label as exact match Click role=button[name*="colour theme"] # ^ matches the aria-label which contains (*=) the substring "colour theme" # other examples: Click role=link[name="Get started"] Fill Text role=textbox[name="Email"] admin@example.com ``` A role selector matches on what the element *is* and what it is *called* — the same two things a screen-reader user navigates by. It is semantic rather than structural, so it survives a redesign that moves the button, restyles it, or rebuilds the surrounding markup. The name here is the **accessible name**, which the browser computes in this order: 1. `aria-labelledby` 2. `aria-label` 3. An associated `