Skip to content

Let libraries built on Browser hand over their own BrowserContext - #5318

Merged
aaltat merged 9 commits into
MarketSquare:mainfrom
d-biehl:adopt-context-hook
Oct 11, 2026
Merged

aaltat merged 9 commits into
MarketSquare:mainfrom
d-biehl:adopt-context-hook

Conversation

@d-biehl

@d-biehl d-biehl commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5317

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.

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.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Automatic cleanup can skip or fail to await the ownership-release callback.

2 open findings
What changed in this PR

Adds JavaScript-extension support for adopting externally created Playwright contexts.

Changes:

  • Adds context adoption, page indexing, and cleanup callbacks.
  • Documents and reserves the injected adoptContext argument.
  • Adds Jest and Robot Framework coverage.
File Description
node/​playwright-wrapper/​playwright-state.ts Implements context adoption and cleanup.
node/​playwright-wrapper/​__tests__/​playwright-state.test.ts Tests adoption and callbacks.
Browser/​browser.py Documents and reserves adoptContext.
atest/​test/​05_JS_Tests/​adopt.js Provides the acceptance-test extension.
atest/​test/​05_JS_Tests/​adopt_context.robot Verifies adopted-context keyword usage.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread node/playwright-wrapper/playwright-state.ts
Comment thread Browser/browser.py Outdated
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.
Comment thread node/playwright-wrapper/playwright-state.ts Outdated
Comment thread node/playwright-wrapper/playwright-state.ts Outdated
@aaltat

aaltat commented Oct 10, 2026

Copy link
Copy Markdown
Member

Looks good in overall, few things I did find, lets discuss.

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.
@d-biehl

d-biehl commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

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?

@d-biehl

d-biehl commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

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.
@d-biehl

d-biehl commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

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.

@d-biehl
d-biehl requested a review from aaltat October 10, 2026 15:45
@d-biehl

d-biehl commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Here is the project that is meant to use these changes: https://github.com/robotcodedev/robotframework-electron-vscode

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Direct extension calls omit the default timeout, and null adopted-browser injections can resolve to reserved marker values.

1 open finding
2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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.

🧠 Review effort: Balanced

Comment thread Browser/browser.py
…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.
@d-biehl

d-biehl commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Trace or coverage failures can bypass context cleanup or the promised onClose callback.

0 open findings

1 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity 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.

Low severity 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.

🧠 Review effort: Balanced

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.
@d-biehl

d-biehl commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Copilot's latest review lists two more points:

  • 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.

@d-biehl

d-biehl commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

That's it from my side for now. I'll leave the branch as it is and only change things again once Tatu has had a chance to review it. 😄😄😄😄😄

@aaltat

aaltat commented Oct 11, 2026

Copy link
Copy Markdown
Member

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.

@aaltat

aaltat commented Oct 11, 2026

Copy link
Copy Markdown
Member

Let me read all the code and comments before commenting anything else, please stand by

@aaltat

aaltat commented Oct 11, 2026

Copy link
Copy Markdown
Member

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

@aaltat
aaltat merged commit ac519c0 into MarketSquare:main Oct 11, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let libraries built on Browser hand over their own BrowserContext

3 participants