Skip to content
Open
6 changes: 4 additions & 2 deletions content/docs/concepts/browser-context-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,10 @@ and `Switch Browser`, `Switch Context` and `Switch Page` move that pointer.
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
one exception is a browser opened by `New Persistent Context` — it is closed
together with its context.
exceptions are a browser opened by `New Persistent Context` and one that a
JavaScript extension
[handed over with `adoptContext`](/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:

Expand Down
166 changes: 156 additions & 10 deletions content/docs/extending/javascript-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ Three conventions carry the whole API:
- **Exported names become keywords** — the *export key*, not the function's own
name. `exports.getLinks = getLinks` gives `Get Links`; the
`exports.myMouseWheel = mouseWheel` below gives `My Mouse Wheel`.
- **Five argument names are filled in for you**, by name — see below.
- **Seven argument names are filled in for you**, by name — see below.
- **`fn.rfdoc` becomes the keyword documentation**, so your keyword shows up in
Libdoc and in editor tooltips like any other.

### The five names Browser fills in
### The seven names Browser fills in

Name a parameter one of these and the library passes the object in. Everything
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 |
Expand All @@ -62,6 +62,8 @@ else in your signature becomes an ordinary keyword argument.
| `browser` | The active [Browser](https://playwright.dev/docs/api/class-browser) |
| `logger` | A function that writes to the Robot Framework log |
| `playwright` | The [`playwright` module](https://playwright.dev/docs/api/class-playwright) itself |
| `adoptContext` | A function that hands a context you created to Browser — see [Handing over a context you created](#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)`
Expand All @@ -72,6 +74,12 @@ 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.

:since{version="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](#handing-over-a-context-you-created).

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
Expand Down Expand Up @@ -119,7 +127,7 @@ exports.registerMySelector = registerMySelector;
*** Test Cases ***
Use A Custom Engine
New Browser chromium
Register My Selector # a browser must already be open
Register My Selector
New Page ${URL}
Click myselector=Some Title
```
Expand All @@ -128,17 +136,154 @@ Use A Custom Engine
---
kind: warning
---
Two things this example depends on.

**A browser must already be open.** Every JavaScript-extension keyword resolves
the active browser before it runs, even one whose only argument is `playwright`,
so calling this first fails with `Browser has been closed.`

**Register once per run.** `selectors.register` rejects a name that is already
registered, and because the registration is awaited that rejection fails the
keyword — unawaited, it takes the whole Node process down instead.
::

## Handing over a context you created

:since{version="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:

```javascript
async function launchElectronApplication(executablePath, playwright, adoptContext, defaultTimeout) {
const app = await playwright._electron.launch({ executablePath });
await app.firstWindow();
app.context().setDefaultTimeout(defaultTimeout);
return adoptContext(app.context(), { name: 'electron' });
}
exports.__esModule = true;
exports.launchElectronApplication = launchElectronApplication;
```

```robot
*** Settings ***
Library Browser jsextension=${CURDIR}/electron.js

*** Test Cases ***
Read The Title Of An Electron Application
Launch Electron Application /path/to/app
Get Title == My App
Close Browser
```

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 Text` and
the other keywords work on it right away. Pages it opens later can be selected
with `Switch Page`.
- **It keeps its own timeout.** Browser does not change the context's default
timeout. To give it the library timeout, as the contexts Browser creates have,
take `defaultTimeout` and call `context.setDefaultTimeout(defaultTimeout)`, as
the example does.
- **`options` describe the browser.** All of them are optional. `name` is what
`Get Browser Catalog` reports as its type, `adopted` by default, and `headless`
says whether it runs headless, `false` by default.
- **`tracing` records a trace.** Pass the absolute path of a `.zip` trace file
or of a folder, which gets `trace_<context id>.zip`, and Browser starts tracing
the context and saves the trace before it closes the context, as with `tracing`
of `New 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](#from-a-library-written-in-python).
- **`contextOptions` say 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: `Download` needs
`acceptDownloads`, which an Electron application has by default, so pass
`{ acceptDownloads: true }` for one.
- **It returns the ids** of the new browser and context, as an object with
`browserId` and `contextId`, plus `pageId` if the context has a page. Return
it from your function and the keyword returns it to Robot Framework.
- **Closing the browser closes the context.** `Close Browser` and automatic
closing treat it like any other browser.
- **`onClose` releases the rest.** The async option `onClose` runs 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`:

```python
from pathlib import Path

from Browser import Browser
from Browser.utils import RecordHar, RecordVideo
from robotlibcore import keyword


class ElectronLibrary(Browser):
@keyword
def launch_electron_application(
self,
executable_path: str,
tracing: bool | Path | None = None,
record_video: RecordVideo | None = None,
record_har: RecordHar | None = None,
):
options = self.adopted_context_options(tracing=tracing, recordVideo=record_video, recordHar=record_har)
adopted = self.call_js_keyword(
"launchElectronApplication",
executablePath=executable_path,
defaultTimeout=int(self.timeout),
**options,
)
self.register_adopted_context(adopted["contextId"], options, adopted.get("videoPath"))
return adopted
```

```javascript
async function launchElectronApplication(executablePath, tracing, recordVideo, recordHar, defaultTimeout, playwright, adoptContext) {
const contextOptions = { acceptDownloads: true };
if (recordVideo) contextOptions.recordVideo = recordVideo;
if (recordHar) contextOptions.recordHar = recordHar;
const app = await playwright._electron.launch({ executablePath, ...contextOptions });
const window = await app.firstWindow();
app.context().setDefaultTimeout(defaultTimeout);
const adopted = await adoptContext(app.context(), { name: 'electron', tracing: tracing || undefined, contextOptions });
return { ...adopted, videoPath: recordVideo ? await window.video()?.path() : null };
}
exports.__esModule = true;
exports.launchElectronApplication = launchElectronApplication;
```

- **`adopted_context_options`** returns an `AdoptedContextOptions`, a typed
dictionary with `tracing`, `recordVideo` and `recordHar` as `New Context`
would use them, `None` when off: `tracing=True` and
`ROBOT_FRAMEWORK_BROWSER_TRACING` give Browser's trace folder, a video goes to
Browser's video folder, 1280×720 by default, and relative paths are resolved
as for `New Context`. Name the function's parameters like its keys and
`**options` passes them on. Give `tracing` to `adoptContext` and the others to
Playwright when you create the context.
- **`register_adopted_context`** registers the context after the call, so that
the keyword calls become groups in the trace and the log embeds the video.
It returns the video path, or an empty string without a video.

::doc-note
---
kind: aside
---
Browser does not support Electron or Android itself, and `_electron` and
`_android` are experimental in Playwright. The module, and keeping it working
with the platform, are yours. A module that does more than launch belongs in a
library of its own, built on Browser.
::

## Debugging

You can attach a Node debugger to the Playwright process — to your own extension
Expand Down Expand Up @@ -230,6 +375,7 @@ rows:
- ['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](#handing-over-a-context-you-created), 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](/docs/extending/python-libraries)']
- ['Your own keywords and your own failure handling, with Browser underneath', '[Browser as a base](/docs/extending/browser-as-a-base)']
Expand Down