You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Let libraries built on Browser hand over their own BrowserContext - #5318
This gives libraries built on Browser a way to hand Browser a BrowserContext that they created themselves, for example for an Electron application or an Android device. Integrations like these can then live in separate projects, and Browser does not need to know about them.
JavaScript extension functions can request adoptContext(context, options), next to page, context, browser, logger and playwright. It adds the context as a new active browser without a browser object, the same way Browser keeps a persistent context. The pages the context already has are indexed, and its first page becomes the active page, so the other keywords work on it right away. It returns the new browser and context ids, and the page id if the context has a page. A function can also request defaultTimeout, the library timeout in milliseconds, and give the context the library timeout with context.setDefaultTimeout(defaultTimeout).
All options are optional:
name: the browser's type in Get Browser Catalog, 'adopted' by default.
headless: false by default.
onClose: an async function that runs once after the context is closed, even if closing the context fails. That lets the library release what the context does not own, such as a device connection.
tracing: the path of a trace file or folder. Browser starts tracing the context and saves the trace before it closes the context, as for New Context with tracing.
contextOptions: the options the context was created with, in the form of Playwright's browser context options. Browser keeps them as it does for New Context, so keywords that depend on them work, such as Download with acceptDownloads.
The changes:
node/playwright-wrapper/playwright-state.ts:
Adds PlaywrightState.adoptContext() and BrowserState.onClose.
BrowserState gets its own input type, { browser, name, headless }. Launched and connected browsers map their browser type to the name.
extensionKeywordCall passes adoptContext to extension functions, and passes injected values that are null or undefined on as they are, instead of the RESERVED marker.
The indexing of existing pages moved into _indexContextWithPages(), which newPersistentContext uses too. So a persistent context now indexes all of its starting pages, not only the first. The first page is still the active one.
closeContext, which automatic closing uses, now awaits closing the browser of a persistent or adopted context, in a finally block. Before, it did not wait, so an async onClose could still run after the keyword returned, and a failing context.close() skipped it. A persistent context whose closing fails is now removed from the stack too.
Browser/browser.py:
adoptContext is a reserved argument name. All reserved names are now in Browser._js_injected_arguments.
Generated extension keywords fill a defaultTimeout parameter with the library timeout at the time of the call.
The jsextension documentation describes adoptContext and its options.
Browser/keywords/playwright_state.py: Get Browser Catalog describes the type of an adopted browser.
Tests:
Jest tests in playwright-state.test.ts.
atest/test/05_JS_Tests/adopt_context.robot: an extension launches a persistent context itself and adopts it. Browser keywords work on it, and Get Browser Catalog reports it as adopted. A second test adopts a context with tracing and contextOptions, downloads a file, and checks that the trace is written.
I verified it locally with inv lint, inv utest-node, inv utest, and inv atest for the suites 05 JS Tests, Persistent State, Video and the suites that use Get Browser Catalog, with Chromium only.
AI / tooling disclosure
This contribution was prepared with assistance from Claude Code (Anthropic). I reviewed the result manually, understand the changes, verified them locally, and confirm that I have the right to submit this contribution under the project license.
Extension functions can request adoptContext(context, onClose) to register a BrowserContext they created, for example with playwright._electron.launch or a persistent context, as a new active browser. onClose runs once after the context is closed. The page indexing is shared with newPersistentContext.
closeContext closed the browser of a persistent or adopted context without awaiting it, so an async onClose could still run after the keyword returned, and a failing context.close() skipped onClose. It now awaits closing the browser in a finally block.
adoptContext(context, options) takes the optional name, headless and onClose. The name is the browser's type in Get Browser Catalog, 'adopted' by default, and headless is false by default. BrowserState gets its own input type, so launched and connected browsers keep their browser type as name.
An adopted context gets the library timeout as its default timeout, like the contexts the library creates. call_js_keyword sends the timeout along with the arguments.
imports.resource imports Browser a second time with other arguments, which Robot Framework warns about. The suite also uses Test Tags instead of the deprecated Force Tags.
One more question, for this PR or a later one. Browser starts tracing for the contexts it creates, with New Context tracing=… or for all contexts with ROBOT_FRAMEWORK_BROWSER_TRACING, and adds the keyword calls as trace groups. An adopted context is never traced, even when tracing is on for all contexts, and there is no keyword to start tracing on an existing context. I think an adopted context should follow Browser's tracing settings, the way it now gets Browser's timeout, rather than get another option. That needs the Python side too (tracing_contexts, trace groups, saving the trace on close). The same goes for HAR recording. Would you like this in this PR, in a follow-up PR, or not at all?
On second thought, after trying it out with the Electron library:
Playwright: Tracing and tracing.startHar() both work on the context of an Electron application.
The trace: The extension cannot save it on its own. onClose runs after the context is closed, and tracing.stop() then fails with "Target page, context or browser has been closed". The trace has to be stopped before the context closes, which Browser already does for contexts that have a trace file.
Tracing as an option: So I'd rather leave the decision to the creator, as with video, and not have Browser apply its settings. The Electron library would get a tracing argument like New Context, and register the context for the keyword groups with add_context_and_keyword_call_stack_to_trace. All Browser needs is a tracing option in adoptContext with the trace path. Browser then starts the trace and saves it on close with its existing code. In a local experiment that was a few lines, and the trace had Browser's actions and screenshots.
HAR: It needs nothing in Browser. tracing.stopHar() still works after the context is closed, so the extension can start the HAR itself and save it in onClose. I checked that the HAR has all requests with their bodies.
Should I add the tracing option to this PR, or would you rather have it in a follow-up?
With tracing, Browser starts tracing the adopted context and saves the trace before it closes the context, as for New Context with tracing. contextOptions are the options the context was created with; Browser keeps them as it does for New Context, so keywords that depend on them work. Download, for example, needs acceptDownloads and failed on every adopted context.
I've added tracing and contextOptions to this PR, so that adoptContext covers what Browser does for the contexts it creates. If you'd rather review them separately, they are one commit and easy to move to a follow-up.
tracing: the path of a trace file or folder. Browser starts tracing the context and saves the trace before it closes it, with its existing code. The Electron library resolves the path as New Context does, including ROBOT_FRAMEWORK_BROWSER_TRACING, and registers the context for the keyword groups.
contextOptions: the options the context was created with, in the form of Playwright's context options. Browser keeps them as it does for New Context. This matters for Download. It checks acceptDownloads in these options, so it failed on every adopted context with "Context acceptDownloads is false", even though an Electron application accepts downloads by default. With contextOptions: { acceptDownloads: true } it works.
The acceptance test adopt_context.robot now also adopts a context with both options, downloads a file and checks that the trace is written. HAR needs nothing in Browser: the Electron library passes recordHar to _electron.launch(), and Playwright writes the file when the context closes.
Preserve null browser values during argument resolution
node/playwright-wrapper/playwright-state.ts:181
Adopted browser states intentionally have browser === null, but the argument resolution immediately below uses apiArguments.get(argName) || namedArguments[argName]. A later extension function that requests browser therefore receives the serialized "RESERVED" marker instead of null and can fail when it treats that value as a Playwright Browser. Select by map membership rather than truthiness so injected null values remain null.
…meout
Extension functions can request defaultTimeout like the other injected names. Generated keywords fill it with the library timeout at the time of the call, so it is sent only to functions that ask for it. adoptContext no longer sets a timeout itself; a function gives its context the library timeout with context.setDefaultTimeout(defaultTimeout). call_js_keyword sends only the arguments it is given.
Injected arguments were resolved with ||, so a null or undefined value fell back to the RESERVED marker. An extension asking for browser got that marker for a persistent context, whose browser is null, and now also for an adopted context. Arguments are now resolved by map membership.
After Copilot's latest review, I changed how an adopted context gets the library timeout: adoptContext no longer sets it. Extension functions can request defaultTimeout instead, like the other injected names, and set it on their context with context.setDefaultTimeout(defaultTimeout). That way the timeout is only sent to functions that ask for it, and nothing else in the extension calls changes. The docs on robotframework-browser.org (MarketSquare/robotframework-browser.org#21) are updated as well.
Copilot also found that injected arguments that are null or undefined fell back to the RESERVED marker, because they were resolved with ||. That was older than this PR, since the browser of a persistent context is null too, and an adopted context now has the same. Arguments are now resolved by map membership, so an extension that asks for browser gets null.
Close adopted context even when trace saving fails
node/playwright-wrapper/playwright-state.ts:748
If saving the trace rejects (for example because the adopted trace path is unwritable), execution never reaches context.c.close(). closeBrowser() has already removed this state and catches the rejection, so the keyword can report the browser as closed while the adopted context remains alive. Close the context in a finally around trace saving so tracing failures cannot leak it.
This issue also appears on line 880 of the same file.
Resolve adopted tracing paths consistently with New Context
Browser/browser.py:531
This claims the same path resolution as New Context, but adoptContext passes options.tracing directly to Node. New Context resolves relative .zip paths under ${OUTPUT_DIR} and folders under browser/traces in Browser/keywords/playwright_state.py:1002-1021; adopted relative paths instead resolve from the Node wrapper cwd (Browser/playwright.py:312-316). Either expose equivalent resolution for adopted contexts or document that this option requires an absolute path.
adoptContext passes options.tracing to Node as given, so a relative path would be relative to the Node process, not resolved like tracing of New Context.
Trace paths of adopted contexts: the docs said the path is resolved as for tracing of New Context, but adoptContext passes it to Node as given, so a relative path would end up relative to the Node process. The docs now ask for an absolute path, here and in Document adoptContext for JavaScript extensions robotframework-browser.org#21. The Electron library passes the path that _resolve_trace_file returns.
Closing a context when saving its trace fails:BrowserState.close() and closeContext() save the trace before they close the context, so a failing save leaves the context open. That is how main behaves for every traced context, including those from tracing of New Context, so I left it out of this PR. If you'd like it changed, I can open a separate PR that closes the context in a finally.
One more question, for this PR or a later one. Browser starts tracing for the contexts it creates, with New Context tracing=… or for all contexts with ROBOT_FRAMEWORK_BROWSER_TRACING, and adds the keyword calls as trace groups. An adopted context is never traced, even when tracing is on for all contexts, and there is no keyword to start tracing on an existing context. I think an adopted context should follow Browser's tracing settings, the way it now gets Browser's timeout, rather than get another option. That needs the Python side too (tracing_contexts, trace groups, saving the trace on close). The same goes for HAR recording. Would you like this in this PR, in a follow-up PR, or not at all?
I did not see that one, good catch. I would like have it in the same style as in Browser, but for me it does not matter is it in this one or in the next PR. You can choose.
I did go trough it. You fixed also few other good things. I will raise issue about those once, so that they get mentioned in release notes. There is are cleanup that needs to be done, but I will merge the issue and do it my self, it is easier to do than try to explain it (just few things about comments and other things which you or no-one else than me could now: #5319 ) Good job
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5317
This gives libraries built on Browser a way to hand Browser a
BrowserContextthat they created themselves, for example for an Electron application or an Android device. Integrations like these can then live in separate projects, and Browser does not need to know about them.JavaScript extension functions can request
adoptContext(context, options), next topage,context,browser,loggerandplaywright. It adds the context as a new active browser without a browser object, the same way Browser keeps a persistent context. The pages the context already has are indexed, and its first page becomes the active page, so the other keywords work on it right away. It returns the new browser and context ids, and the page id if the context has a page. A function can also requestdefaultTimeout, the library timeout in milliseconds, and give the context the library timeout withcontext.setDefaultTimeout(defaultTimeout).All options are optional:
name: the browser's type inGet Browser Catalog,'adopted'by default.headless:falseby default.onClose: an async function that runs once after the context is closed, even if closing the context fails. That lets the library release what the context does not own, such as a device connection.tracing: the path of a trace file or folder. Browser starts tracing the context and saves the trace before it closes the context, as forNew Contextwithtracing.contextOptions: the options the context was created with, in the form of Playwright's browser context options. Browser keeps them as it does forNew Context, so keywords that depend on them work, such asDownloadwithacceptDownloads.The changes:
node/playwright-wrapper/playwright-state.ts:PlaywrightState.adoptContext()andBrowserState.onClose.BrowserStategets its own input type,{ browser, name, headless }. Launched and connected browsers map their browser type to the name.extensionKeywordCallpassesadoptContextto extension functions, and passes injected values that arenullorundefinedon as they are, instead of theRESERVEDmarker._indexContextWithPages(), whichnewPersistentContextuses too. So a persistent context now indexes all of its starting pages, not only the first. The first page is still the active one.closeContext, which automatic closing uses, now awaits closing the browser of a persistent or adopted context, in afinallyblock. Before, it did not wait, so an asynconClosecould still run after the keyword returned, and a failingcontext.close()skipped it. A persistent context whose closing fails is now removed from the stack too.Browser/browser.py:adoptContextis a reserved argument name. All reserved names are now inBrowser._js_injected_arguments.defaultTimeoutparameter with the library timeout at the time of the call.jsextensiondocumentation describesadoptContextand its options.Browser/keywords/playwright_state.py:Get Browser Catalogdescribes the type of an adopted browser.playwright-state.test.ts.atest/test/05_JS_Tests/adopt_context.robot: an extension launches a persistent context itself and adopts it. Browser keywords work on it, andGet Browser Catalogreports it asadopted. A second test adopts a context withtracingandcontextOptions, downloads a file, and checks that the trace is written.I verified it locally with
inv lint,inv utest-node,inv utest, andinv atestfor the suites05 JS Tests,Persistent State,Videoand the suites that useGet Browser Catalog, with Chromium only.The guide on JavaScript extensions on robotframework-browser.org is updated in MarketSquare/robotframework-browser.org#21.
AI / tooling disclosure
This contribution was prepared with assistance from Claude Code (Anthropic). I reviewed the result manually, understand the changes, verified them locally, and confirm that I have the right to submit this contribution under the project license.