Skip to content
Merged
56 changes: 43 additions & 13 deletions Browser/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,15 @@ class Browser(DynamicCore):
_context_cache = ContextCache()
_suite_cleanup_done = False
_output_dir = "."
# Parameter names the Node side fills in for JavaScript extension functions.
_js_injected_arguments = (
"logger",
"playwright",
"page",
"context",
"browser",
"adoptContext",
)

def __init__( # noqa: PLR0915
self,
Expand Down Expand Up @@ -501,10 +510,32 @@ def __init__( # noqa: PLR0915
- ``jsextension``: Path to JavaScript modules exposed as extra keywords. The
modules must be in CommonJS format; exported functions become keywords
and an ``fn.rfdoc`` string becomes a keyword's documentation. The
argument names ``page``, ``context``, ``browser``, ``logger`` and
``playwright`` are filled in by the library rather than taken from the
keyword call. Can be a single path, a comma-separated list of paths or a
real list of strings. See
argument names ``page``, ``context``, ``browser``, ``logger``,
``playwright``, ``adoptContext`` and ``defaultTimeout`` are filled in by
the library rather than taken from the keyword call. ``defaultTimeout``
is the library timeout in milliseconds. ``adoptContext(context, options)``
registers a BrowserContext that the function created itself, for
example with ``playwright.chromium.launchPersistentContext`` or as
``app.context()`` of an app from ``playwright._electron.launch``, as a
new active browser and makes its first page the active page, so the
other keywords work on it. It keeps its own default timeout;
``context.setDefaultTimeout(defaultTimeout)`` gives it the library
timeout, like the contexts the library creates. It returns the new
browser and context ids, and the page id if the context has a page.
Closing that browser closes the context. The optional ``options`` are
``name``, which `Get Browser Catalog` reports as the browser's type,
``adopted`` by default; ``headless``, ``false`` by default;
``onClose``, an async function that runs once after the context is
closed, so the function can release what the context does not own,
such as a device connection; ``tracing``, the absolute path of a
``.zip`` trace file or of a folder, in which the trace is saved as
``trace_<context id>.zip``; and ``contextOptions``, the options the
context was created with, in the form of Playwright's browser context
options. With ``tracing``, the library starts tracing the context and
saves the trace before it closes the context. With ``contextOptions``,
keywords that depend on them work on the context, such as `Download`
with ``acceptDownloads``. Can be a single path, a comma-separated list
of paths or a real list of strings. See
https://robotframework-browser.org/docs/extending/javascript-extensions
- ``language``: Defines language which is used to translate keyword names and
documentation.
Expand Down Expand Up @@ -802,8 +833,11 @@ def _jskeyword_call(
arg_set_texts = []
for item in argument_names_and_vals:
arg_name = item[0]
if arg_name in ["logger", "playwright", "page", "context", "browser"]:
if arg_name in self._js_injected_arguments:
Comment thread
d-biehl marked this conversation as resolved.
arg_set_texts.append(f'("{arg_name}", "RESERVED")')
elif arg_name == "defaultTimeout":
# The library timeout at the time of the call.
arg_set_texts.append('("defaultTimeout", int(self.timeout))')
else:
arg_set_texts.append(f'("{arg_name}", {arg_name})')
if arg_name == "args":
Expand Down Expand Up @@ -852,16 +886,12 @@ def {name}(self, {", ".join(argument_names_and_default_values_texts)}):
raise DataError(f"{e.msg} in {name}")

def call_js_keyword(self, keyword_name: str, **args) -> Any:
reserved = {
"logger": "RESERVED",
"playwright": "RESERVED",
"page": "RESERVED",
"context": "RESERVED",
"browser": "RESERVED",
}
_args_browser_internal = {
"arguments": [
(arg_name, reserved.get(arg_name, value))
(
arg_name,
"RESERVED" if arg_name in self._js_injected_arguments else value,
)
for arg_name, value in args.items()
]
}
Expand Down
5 changes: 4 additions & 1 deletion Browser/keywords/playwright_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1247,7 +1247,10 @@ def get_browser_catalog(

The data is parsed into a python list containing data representing the open Objects.

On the root level the data contains a list of open browsers.
On the root level the data contains a list of open browsers. The ``type`` of a
browser is ``chromium``, ``firefox`` or ``webkit``. For a context that a JavaScript
extension adopted, it is the name the extension passed, ``adopted`` by default;
see ``jsextension`` in `Importing`.

Data can be manipulated also with ``assertion_operator`` for example to find
a specific id based on index or page title with ``then`` operator.
Expand Down
51 changes: 51 additions & 0 deletions atest/test/05_JS_Tests/adopt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
let adoptedContextClosed = false;
let onCloseCalled = false;

async function openAdoptedPersistentContext(userDataDir, url, headless, playwright, adoptContext, defaultTimeout) {
const context = await playwright.chromium.launchPersistentContext(userDataDir, {
headless: String(headless).toLowerCase() !== 'false',
});
context.setDefaultTimeout(defaultTimeout);
context.on('close', () => (adoptedContextClosed = true));
await context.pages()[0].goto(url);
return adoptContext(context, {
onClose: async () => {
onCloseCalled = true;
},
});
}

async function openAdoptedPersistentContextWithTracingAndDownloads(userDataDir, url, headless, tracing, playwright, adoptContext) {
const contextOptions = { acceptDownloads: true };
const context = await playwright.chromium.launchPersistentContext(userDataDir, {
headless: String(headless).toLowerCase() !== 'false',
...contextOptions,
});
await context.pages()[0].goto(url);
return adoptContext(context, { tracing, contextOptions });
}

async function millisecondsUntilAWaitFails(page) {
const start = Date.now();
try {
await page.waitForSelector('#not-on-the-page');
} catch {
return Date.now() - start;
}
return -1;
}

async function adoptedContextIsClosed() {
return adoptedContextClosed;
}

async function adoptedContextOnCloseWasCalled() {
return onCloseCalled;
}

exports.__esModule = true;
exports.openAdoptedPersistentContext = openAdoptedPersistentContext;
exports.openAdoptedPersistentContextWithTracingAndDownloads = openAdoptedPersistentContextWithTracingAndDownloads;
exports.millisecondsUntilAWaitFails = millisecondsUntilAWaitFails;
exports.adoptedContextIsClosed = adoptedContextIsClosed;
exports.adoptedContextOnCloseWasCalled = adoptedContextOnCloseWasCalled;
34 changes: 34 additions & 0 deletions atest/test/05_JS_Tests/adopt_context.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
*** Settings ***
Library Browser jsextension=${CURDIR}/adopt.js
Library OperatingSystem
Resource ../variables.resource

Test Tags no-iframe

*** Test Cases ***
Browser Keywords Work On A Context Adopted By A JavaScript Extension
${adopted} = Open Adopted Persistent Context ${OUTPUT_DIR}/adopted-profile ${LOGIN_URL} ${HEADLESS}
Get Browser Ids ACTIVE contains ${adopted}[browserId]
Get Browser Catalog validate [b["type"] for b in value if b["id"] == "${adopted}[browserId]"] == ["adopted"]
Get Text h1 == Login Page
Close Browser
${closed} = Adopted Context Is Closed
Should Be True ${closed}
${on_close_called} = Adopted Context On Close Was Called
Should Be True ${on_close_called}

An Extension Gives An Adopted Context The Library Timeout
Set Browser Timeout 1s scope=Test
Open Adopted Persistent Context ${OUTPUT_DIR}/adopted-profile-timeout ${LOGIN_URL} ${HEADLESS}
${elapsed} = Milliseconds Until A Wait Fails
Should Be True ${elapsed} < 5000
[Teardown] Close Browser

An Adopted Context Is Traced And Accepts Downloads
Open Adopted Persistent Context With Tracing And Downloads
... ${OUTPUT_DIR}/adopted-profile-traced ${LOGIN_URL} ${HEADLESS} ${OUTPUT_DIR}/adopted-trace.zip
${download_url} = Get Property id=file_download href
${download} = Download ${download_url}
File Should Exist ${download}[saveAs]
Close Browser
File Should Exist ${OUTPUT_DIR}/adopted-trace.zip
Loading
Loading