Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@
loaded (on CDP's `Network.loadingFinished`, not `Network.responseReceived`, so `exchange.response.body` is always
available), yielding the request's `Network::Exchange`. Doesn't require `network.intercept` to be set up, and is
never fired for requests that fail to load [#294]
- `Ferrum::Frame#evaluate` (and `Page`/`Browser`/`Node`) accept **named arguments**, which become the script's
function parameters in order, so scripts name what they receive instead of reaching into `arguments[0]`:
`page.evaluate("a + b", a: 1, b: 2)`. Each keyword is sent as its own protocol argument, so a `Ferrum::Node`
still arrives in JavaScript as the live element. When the script is a function declaration, values are bound
to its own parameter names rather than to hash order
- `#evaluate`/`#execute`/`#evaluate_handle` accept either a bare expression, which is wrapped for you, or a
function/arrow declaration, which is used as-is. This folds `#evaluate_func` into `#evaluate` and is how you
run multi-statement scripts
- Promises are now always awaited (`awaitPromise`), so `page.evaluate("await fetch(url)", url: "/x")` works and
there is no need for a separate asynchronous method. A `timeout:` keyword (seconds, defaulting to the page
timeout) bounds how long a script may take before `Ferrum::ScriptTimeoutError`; `0` disables it
- `Ferrum::Frame#evaluate_handle` and `Ferrum::Node#evaluate_handle` return a `Ferrum::RemoteObject`, an opaque
reference to a browser-side value that can be passed straight back in as an argument without being serialized
- `Ferrum::Node#execute` and `Ferrum::Node#evaluate_handle`, matching the page-level API but with `this` bound
to the node
- `FERRUM_DEPRECATION_WARNINGS` controls the new deprecation warnings: `raise` turns them into errors while
migrating a suite, `0` silences them
- `Ferrum::Browser#quit`/`Ferrum::Browser::Process#stop` accept `wait: false` to return immediately and run process
killing and user-data-directory cleanup on a background thread instead of blocking; the call returns the `Thread`
so callers can `#join` it if they need cleanup to have finished, e.g. before process exit or before reusing a
Expand All @@ -16,6 +33,15 @@

### Changed
- `Ferrum::Page::Stream#stream` now attempts to close the CDP stream handle (`IO.close`) once it's been fully read.
- `Ferrum::Frame#evaluate_async`, `#evaluate_func` and `#evaluate_on` are deprecated and warn when called; so does
passing positional arguments to `#evaluate`/`#execute`. All of them keep working — `arguments[n]` is still
populated inside the generated function — and will be removed in the next major release. Replacements:
`evaluate_async(expr, wait, *args)` becomes `evaluate("await …", timeout: wait)`, `evaluate_func(fn, *args)`
becomes `evaluate(fn, name: value)`, and `evaluate_on(node:, expression:)` becomes `node.evaluate(expression)`
- `Ferrum::Node#evaluate` resolves its result the same way `Page#evaluate` does, so `node.evaluate("this.parentNode")`
returns a `Ferrum::Node` instead of an empty hash. It previously ran with `returnByValue`, which flattened
every DOM result
- `Ferrum::Frame#evaluate` now awaits a returned promise instead of serializing it to an empty object

### Fixed
- `#evaluate`/`#evaluate_on`/etc. resolved an object/array result by making two CDP round trips
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,60 @@ JS
browser.quit
```

Pass arguments by name, so the script can use parameters instead of digging
through `arguments[0]`:

```ruby
page.evaluate("a + b", a: 1, b: 2) # => 3
page.evaluate("el.getAttribute(name)", el: page.at_css("a"), name: "href")
```

A script that starts with a function or arrow declaration is used as-is, which
is how you run more than one statement:

```ruby
page.evaluate(<<~JS, c: 3)
function(a, b) {
const sum = a + b;
return sum * c;
}
JS
```

Promises are always awaited, so `async`/`await` works directly:

```ruby
page.evaluate("await fetch(url).then(r => r.text())", url: "/api")
page.evaluate("new Promise(resolve => setTimeout(() => resolve(42), 100))") # => 42
```

Pass `timeout:` (seconds, defaulting to the page timeout) to bound how long the
script may take before `Ferrum::ScriptTimeoutError` is raised. `timeout:` and
`args:` are the only reserved keywords; use `args:` when a JavaScript parameter
needs one of those names:

```ruby
page.evaluate("timeout * 2", args: { timeout: 21 }) # => 42
```

Use `#execute` when you only want the side effects, `#evaluate_handle` when you
want to keep a value in the browser and pass it back in later, and
`Ferrum::Node#evaluate` to run a script with `this` bound to an element:

```ruby
page.execute("window.scrollBy(0, 100)") # => true

list = page.evaluate_handle("document.querySelectorAll('li')")
page.evaluate("Array.from(nodes).map(n => n.textContent)", nodes: list)

page.at_css("input").evaluate("this.value")
```

`#evaluate_async`, `#evaluate_func` and `#evaluate_on` are deprecated in favour
of the above and warn when called. Positional arguments still work but warn too.
Set `FERRUM_DEPRECATION_WARNINGS=raise` to turn the warnings into errors while
migrating, or `=0` to silence them.

Do any mouse movements you like:

```ruby
Expand Down
2 changes: 2 additions & 0 deletions lib/ferrum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
require "ferrum/utils/platform"
require "ferrum/utils/elapsed_time"
require "ferrum/utils/attempt"
require "ferrum/utils/deprecate"
require "ferrum/errors"
require "ferrum/browser"
require "ferrum/remote_object"
require "ferrum/node"

#
Expand Down
3 changes: 2 additions & 1 deletion lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class Browser
screenshot pdf mhtml viewport_size device_pixel_ratio
start_screencast stop_screencast
frames frame_by main_frame
evaluate evaluate_on evaluate_async execute evaluate_func
evaluate evaluate_handle execute evaluate_in
evaluate_on evaluate_async evaluate_func
add_script_tag add_style_tag bypass_csp
on position position=
playback_rate playback_rate=
Expand Down
7 changes: 3 additions & 4 deletions lib/ferrum/frame.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,11 @@ def parent
# frame.body # => <html><head></head><body><p>lol</p></body></html>
#
def content=(html)
evaluate_async(%(
execute(<<~JS, html: html)
document.open();
document.write(arguments[0]);
document.write(html);
document.close();
arguments[1](true);
), @page.timeout, html)
JS
@page.document_node_id
end
alias set_content content=
Expand Down
94 changes: 50 additions & 44 deletions lib/ferrum/frame/dom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,38 @@ class Frame
#
module DOM
SCRIPT_SRC_TAG = <<~JS
const script = document.createElement("script");
script.src = arguments[0];
script.type = arguments[1];
script.onload = arguments[2];
document.head.appendChild(script);
function(url, type) {
const script = document.createElement("script");
script.src = url;
script.type = type;
document.head.appendChild(script);
return new Promise(resolve => script.onload = resolve);
}
JS
SCRIPT_TEXT_TAG = <<~JS
const script = document.createElement("script");
script.text = arguments[0];
script.type = arguments[1];
document.head.appendChild(script);
arguments[2]();
function(content, type) {
const script = document.createElement("script");
script.text = content;
script.type = type;
document.head.appendChild(script);
}
JS
STYLE_TAG = <<~JS
const style = document.createElement("style");
style.type = "text/css";
style.appendChild(document.createTextNode(arguments[0]));
document.head.appendChild(style);
arguments[1]();
function(content) {
const style = document.createElement("style");
style.type = "text/css";
style.appendChild(document.createTextNode(content));
document.head.appendChild(style);
}
JS
LINK_TAG = <<~JS
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = arguments[0];
link.onload = arguments[1];
document.head.appendChild(link);
function(url) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = url;
document.head.appendChild(link);
return new Promise(resolve => link.onload = resolve);
}
JS

#
Expand Down Expand Up @@ -159,7 +165,7 @@ def xpath(selector, within: nil)
}
JS

evaluate_func(expr, selector, within)
evaluate(expr, selector: selector, within: within)
end

#
Expand All @@ -186,7 +192,7 @@ def at_xpath(selector, within: nil)
return xpath.snapshotItem(0);
}
JS
evaluate_func(expr, selector, within)
evaluate(expr, selector: selector, within: within)
end

#
Expand All @@ -213,7 +219,7 @@ def css(selector, within: nil)
}
JS

evaluate_func(expr, selector, within)
evaluate(expr, selector: selector, within: within)
end

#
Expand All @@ -240,7 +246,7 @@ def at_css(selector, within: nil)
}
JS

evaluate_func(expr, selector, within)
evaluate(expr, selector: selector, within: within)
end

#
Expand Down Expand Up @@ -306,17 +312,17 @@ def wait_for_selector(css: nil, xpath: nil, within: nil, timeout: @page.timeout,
# browser.add_script_tag(url: "http://example.com/stylesheet.css") # => true
#
def add_script_tag(url: nil, path: nil, content: nil, type: "text/javascript")
expr, *args = if url
[SCRIPT_SRC_TAG, url, type]
elsif path || content
if path
content = File.read(path)
content += "\n//# sourceURL=#{path}"
end
[SCRIPT_TEXT_TAG, content, type]
end
if url
evaluate(SCRIPT_SRC_TAG, url: url, type: type)
elsif path || content
if path
content = File.read(path)
content += "\n//# sourceURL=#{path}"
end
evaluate(SCRIPT_TEXT_TAG, content: content, type: type)
end

evaluate_async(expr, @page.timeout, *args)
true
end

#
Expand All @@ -332,17 +338,17 @@ def add_script_tag(url: nil, path: nil, content: nil, type: "text/javascript")
# browser.add_style_tag(content: "h1 { font-size: 40px; }") # => true
#
def add_style_tag(url: nil, path: nil, content: nil)
expr, *args = if url
[LINK_TAG, url]
elsif path || content
if path
content = File.read(path)
content += "\n//# sourceURL=#{path}"
end
[STYLE_TAG, content]
end
if url
evaluate(LINK_TAG, url: url)
elsif path || content
if path
content = File.read(path)
content += "\n//# sourceURL=#{path}"
end
evaluate(STYLE_TAG, content: content)
end

evaluate_async(expr, @page.timeout, *args)
true
end
end
end
Expand Down
Loading
Loading