feat: redesign the JavaScript evaluation API around named arguments - #617
Open
route wants to merge 1 commit into
Open
feat: redesign the JavaScript evaluation API around named arguments#617route wants to merge 1 commit into
route wants to merge 1 commit into
Conversation
`#evaluate`, `#evaluate_async`, `#execute`, `#evaluate_func` and `#evaluate_on`
had four different argument conventions and three different return semantics
between them. Scrip`#evaluate_async` spliced its resolve callback in at `arguments[arguments.length]`,
so the callback's index shifted with the number of arguments passed. Neither
Puppeteer nor Playwright ships an asynchronous variant at all — both always await
the returned promise — and CDP has `awaitPromise`, so none of that machinery was
still earning its place.
The API is now three entry points that share one script shape and one argument
style, plus the same three on `Node` with `this` bound to the element:
- Keyword arguments become the script's function parameters, in order:
`page.evaluate("a + b", a: 1, b: 2)`. Each is sent as its own protocol
argument, so a `Node` still arrives in JavaScript as the live element. When
the script is a function declaration, values bind to its own parameter names
rather than to hash order.
- A script is either a bare expression, which gets wrapped, or a function/arrow
declaration, which is used as-is. This folds `#evaluate_func` into `#evaluate`
and is how multi-statement scripts are written.
- Promises are always awaited, so `evaluate("await fetch(url)", url: "/x")` works
without a separate method. `timeout:` (seconds, defaulting to the page timeout)
bounds the script browser-side and raises `ScriptTimeoutError`; `0` disables it.
- `#evaluate_handle` returns a `Ferrum::RemoteObject`, an opaque reference to a
browser-side value that can be passed straight back in as an argument.
- Added `Node#execute` and `Node#evaluate_handle`.ts reached their arguments through `arguments[0]`, and
Nothing breaks. `arguments[n]` is still populated inside the generated function,
`#evaluate_on` remain as shims. All four paths warn once per message and call
site; `FERRUM_DEPRECATION_WARNINGS=raise` turns the warnings into errors while
migrating a suite, `=0` silences them.
Two behaviour changes worth noting: `Node#evaluate` resolves its result the way
`Page#evaluate` does, so `node.evaluate("this.parentNode")` returns a `Node`
instead of an empty hash — it previously ran with `returnByValue`, which
flattened every DOM result — and `#evaluate` now awaits a returned promise
instead of serializing it to an empty object.
Detection of a function declaration is deliberately conservative: a script
beginning with `function` counts as one only if it ends at the closing brace,
so the IIFE `function() { ... }()` stays an expression, and arrow detection
accepts only a plain identifier parameter list, so `(() => 1)()` does too.
This was referenced Aug 23, 2026
Closed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
#evaluate,#evaluate_async,#execute,#evaluate_funcand#evaluate_onhad four different argument conventions and three different return semantics between them. Scrip#evaluate_asyncspliced its resolve callback in atarguments[arguments.length], so the callback's index shifted with the number of arguments passed. Neither Puppeteer nor Playwright ships an asynchronous variant at all — both always await the returned promise — and CDP hasawaitPromise, so none of that machinery was still earning its place.The API is now three entry points that share one script shape and one argument style, plus the same three on
Nodewiththisbound to the element:page.evaluate("a + b", a: 1, b: 2). Each is sent as its own protocol argument, so aNodestill arrives in JavaScript as the live element. When the script is a function declaration, values bind to its own parameter names rather than to hash order.#evaluate_funcinto#evaluateand is how multi-statement scripts are written.evaluate("await fetch(url)", url: "/x")works without a separate method.timeout:(seconds, defaulting to the page timeout) bounds the script browser-side and raisesScriptTimeoutError;0disables it.#evaluate_handlereturns aFerrum::RemoteObject, an opaque reference to a browser-side value that can be passed straight back in as an argument.Node#executeandNode#evaluate_handle.ts reached their arguments througharguments[0], andNothing breaks.
arguments[n]is still populated inside the generated function,#evaluate_onremain as shims. All four paths warn once per message and call site;FERRUM_DEPRECATION_WARNINGS=raiseturns the warnings into errors while migrating a suite,=0silences them.Two behaviour changes worth noting:
Node#evaluateresolves its result the wayPage#evaluatedoes, sonode.evaluate("this.parentNode")returns aNodeinstead of an empty hash — it previously ran withreturnByValue, which flattened every DOM result — and#evaluatenow awaits a returned promise instead of serializing it to an empty object.Detection of a function declaration is deliberately conservative: a script beginning with
functioncounts as one only if it ends at the closing brace, so the IIFEfunction() { ... }()stays an expression, and arrow detection accepts only a plain identifier parameter list, so(() => 1)()does too.