Skip to content

feat(runtime): add a JavaScript evaluation API with named arguments, keeping the old one with warnings - #659

Open
route wants to merge 1 commit into
mainfrom
feat/js-api-compat
Open

route wants to merge 1 commit into
mainfrom
feat/js-api-compat

Conversation

@route

@route route commented Oct 19, 2026

Copy link
Copy Markdown
Member

First of two steps that redesign the JavaScript evaluation API. This one ships in 0.20 and changes no existing result; the next one, for 0.21, removes the old API.

New API

page.evaluate("a + b", a: 1, b: 2)                              # named arguments become parameters
page.evaluate("function(a, b) { return a - b }", b: 1, a: 3)    # a declaration is used as-is, bound by name
page.evaluate("await fetch(url).then(r => r.status)", url: "/") # await works, timeout: bounds it
list = page.evaluate_handle("document.querySelectorAll('li')") # => Ferrum::RemoteObject
node.execute("this.value = text", text: "hi")                   # Node#execute, Node#evaluate_handle
  • Scripts are read by a small scanner (Frame::Script) that skips strings, template literals, regular expressions and comments.
  • The timeout is enforced in Ruby rather than with a Promise.race in the page, so pages that replace Promise or mock timers don't break it. ScriptTimeoutError is now a TimeoutError, so rescue Ferrum::TimeoutError keeps catching it.

Compatibility

A call takes the new path only if it uses something only the new API has: named arguments, a function declaration, await, timeout:, or a new method. Anything else runs the code from main unchanged, in Frame::Runtime::Legacy. That includes evaluate_async, evaluate_func and evaluate_on. Positional arguments keep working, now and after 0.21.

For old-style calls, Ferrum detects what 0.21 will do differently and warns with a suggested rewrite (the full table is in docs/13-javascript.md, "Upgrading to 0.21"):

Detected How
Keywords read through arguments, or mixed with positional arguments arguments given
Several statements, "a(); b" Runtime.compileScript of the 0.21 wrapper, cached per script
A script after a leading line break never runs today scanner
A promise or thenable will be awaited result subtype, or a then check on object results
A thrown non-Error will raise exceptionDetails with a non-error result
Node#evaluate will resolve nodes and dates result subtype

Warnings point at the first caller outside installed gems, which for Capybara users is the line calling execute_script. They print once per call site. FERRUM_DEPRECATION_WARNINGS=raise turns them into errors, and 0 silences them.

Verified

A script comparing about 40 old-style calls against main returns identical results. The one exception is evaluate("new Error('x')"), which used to crash with NoMethodError and now raises JavaScriptError. spec/frame/runtime/legacy_spec.rb pins those results and warnings.

…keeping the old one with warnings

`#evaluate`, `#evaluate_async`, `#execute`, `#evaluate_func` and `#evaluate_on` had four argument conventions and three
return semantics between them, and scripts reached their arguments through `arguments[0]`. This adds one convention for
all of them, without changing what any existing call returns.

New: keyword arguments become the script's parameters, `page.evaluate("a + b", a: 1, b: 2)`, bound by name when the
script is a function declaration. A script may be a function or arrow declaration, used as-is, and may `await`; promises
it returns are awaited, bounded by `timeout:` and raising `ScriptTimeoutError`, now a `TimeoutError`. Added
`#evaluate_handle`, returning a `Ferrum::RemoteObject`, and `Node#execute`/`Node#evaluate_handle`. Scripts are read by a
small scanner, `Frame::Script`, that skips strings, template literals, regular expressions and comments.

Compatible: a call that doesn't use any of that runs exactly as before, through `Runtime::Legacy`, which also keeps
`#evaluate_async`, `#evaluate_func` and `#evaluate_on`. Positional arguments keep working. What 0.21 will do differently
is detected and warned about with a rewrite, pointing at the first caller outside installed gems: keywords read through
`arguments`, mixed positional and keyword arguments, scripts of several statements or that never run, promises and
thenables that will be awaited, thrown non-Errors that will raise, and nodes `Node#evaluate` will resolve.
`FERRUM_DEPRECATION_WARNINGS=raise` turns the warnings into errors, `0` silences them.
@route
route force-pushed the feat/js-api-compat branch from 438925a to 087049a Compare October 21, 2026 19:20
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.

1 participant