# Technical Documentation: rubycdp/ferrum

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/rubycdp/ferrum)

### Overview (`README.md`)

# Ferrum - high-level API to control Chrome in Ruby

## [Documentation](https://docs.rubycdp.com/docs/ferrum/introduction)

<img align="right"
     width="160" height="160"
     alt="Ferrum logo"
     src="https://raw.githubusercontent.com/rubycdp/ferrum/main/logo.svg?sanitize=true">

It is Ruby clean and high-level API to Chrome. Runs headless by default, but you
can configure it to run in a headful mode. All you need is Ruby and [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/).
Ferrum connects to the browser by [CDP protocol](https://chromedevtools.github.io/devtools-protocol/) and  there's _no_
Selenium/WebDriver/ChromeDriver dependency. The emphasis was made on a raw CDP
protocol because Chrome allows you to do so many things that are barely
supported by WebDriver because it should have consistent design with other
browsers.

* [Cuprite](https://github.com/rubycdp/cuprite) is a pure Ruby driver for [Capybara](https://github.com/teamcapybara/capybara) based on Ferrum.
* [Vessel](https://github.com/rubycdp/vessel) high-level web crawling framework based on Ferrum and Mechanize.

<br clear="right">

## Install

There's no official Chrome or Chromium package for Linux don't install it this
way because it's either outdated or unofficial, both are bad. Download it from
official source for [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/getting-involved/download-chromium).
Chrome binary should be in the `PATH` or `BROWSER_PATH` and you can pass it as an
option to browser instance see `:browser_path` in
[Customization](https://docs.rubycdp.com/docs/ferrum/customization).

Add this to your `Gemfile` and run `bundle install`.

``` ruby
gem "ferrum"
```

Or skip installing Chrome and run it in a container with our images, see
[Docker](https://docs.rubycdp.com/docs/ferrum/docker#our-images-recommended):

```shell
docker run --rm -p 3000:3000 ghcr.io/rubycdp/chromium
```

```ruby
browser = Ferrum::Browser.new(url: "http://localhost:3000")
```

## Quick Start

Navigate to a website and save a screenshot:

```ruby
browser = Ferrum::Browser.new
browser.go_to("https://google.com")
browser.screenshot(path: "google.png")
browser.quit
```

When you work with browser instance Ferrum creates and maintains a default page for you, in fact all the methods above
are sent to the `page` instance that is created in the `default_context` of the `browser` instance. You can interact
with a page created manually and this is preferred:

```ruby
browser = Ferrum::Browser.new
page = browser.create_page
page.go_to("https://google.com")
input = page.at_xpath("//input[@name='q']")
input.focus.type("Ruby headless driver for Chrome", :Enter)
page.at_css("a > h3").text # => "rubycdp/ferrum: Ruby Chrome/Chromium driver - GitHub"
browser.quit
```

Evaluate some JavaScript and get full width/height:

```ruby
browser = Ferrum::Browser.new
page = browser.create_page
page.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
width, height = page.evaluate <<~JS
  [document.documentElement.offsetWidth,
   document.documentElement.offsetHeight]
JS
# => [1024, 1931]
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 is a function or arrow declaration is used as-is and takes its
arguments positionally, in the order it declares them:

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

`await` works directly, and a function declaration's returned promise is
awaited:

```ruby
page.evaluate("await fetch(url).then(r => r.text())", url: "/api")
```

Pass `timeout:` (seconds, defaulting to the page timeout) to bound how long the
script may take before `Ferrum::ScriptTimeoutError` is raised. `timeout:` is the
only reserved keyword.

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")
```

Scripts written for the old API keep working, and Ferrum warns, with a suggested
rewrite, about any whose result will change. The next release, 0.21, removes the
old behavior, so run your suite with `FERRUM_JS_API=new` to check it's ready. See
[upgrading to 0.21](docs/13-javascript.md#upgrading-to-021).

Do any mouse movements you like:

```ruby
# Trace a 100x100 square
browser = Ferrum::Browser.new
page = browser.create_page
page.go_to("https://google.com")
page.mouse
  .move(x: 0, y: 0)
  .down
  .move(x: 0, y: 100)
  .move(x: 100, y: 100)
  .move(x: 100, y: 0)
  .move(x: 0, y: 0)
  .up

browser.quit
```

## Development

After checking out the repo, run `bundle install` to install dependencies.

Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will
allow you to experiment.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).


## Contributing

Bug reports and pull requests are welcome on [GitHub](https://github.com/rubycdp/ferrum).

## License

The gem is available as open source under the terms of the
[MIT License](https://opensource.org/licenses/MIT).

---

### 1 Introduction (`docs/1-introduction.md`)

---
sidebar_position: 1
---

# Introduction

Ferrum is a high-level API to control Chrome in Ruby.

<img align="right"
     width="160" height="160"
     alt="Ferrum logo"
     src="https://raw.githubusercontent.com/rubycdp/ferrum/main/logo.svg?sanitize=true" />

It is Ruby clean and high-level API to Chrome. Runs headless by default, but you
can configure it to run in a headful mode. All you need is Ruby and [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/).
Ferrum connects to the browser by [CDP protocol](https://chromedevtools.github.io/devtools-protocol/) and  there's _no_
Selenium/WebDriver/ChromeDriver dependency. The emphasis was made on a raw CDP
protocol because Chrome allows you to do so many things that are barely
supported by WebDriver because it should have consistent design with other
browsers.

<br clear="right" />

## Installation

There's no official Chrome or Chromium package for Linux don't install it this
way because it's either outdated or unofficial, both are bad. Download it from
official source for [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/getting-involved/download-chromium).
Chrome binary should be in the `PATH` or `BROWSER_PATH` and you can pass it as an
option to browser instance see `:browser_path` in
[Customization](/docs/ferrum/customization).

Add this to your `Gemfile` and run `bundle install`.

``` ruby
gem "ferrum"
```

## Docker

To skip installing Chrome, run it in a container with our images, `ghcr.io/rubycdp/chrome` or
`ghcr.io/rubycdp/chromium`, and connect with `Ferrum::Browser.new(url: "http://localhost:3000")`. See
[Docker](/docs/ferrum/docker) for that and for running Chrome in your own container.

## Quick Start

Navigate to a website and save a screenshot:

```ruby
browser = Ferrum::Browser.new
browser.go_to("https://google.com")
browser.screenshot(path: "google.png")
browser.quit
```

When you work with browser instance Ferrum creates and maintains a default page for you, in fact all the methods above
are sent to the `page` instance that is created in the `default_context` of the `browser` instance. You can interact
with a page created manually and this is preferred:

```ruby
browser = Ferrum::Browser.new
page = browser.create_page
page.go_to("https://google.com")
input = page.at_xpath("//input[@name='q']")
input.focus.type("Ruby headless driver for Chrome", :Enter)
page.at_css("a > h3").text # => "rubycdp/ferrum: Ruby Chrome/Chromium driver - GitHub"
browser.quit
```

Evaluate some JavaScript and get full width/height:

```ruby
browser = Ferrum::Browser.new
page = browser.create_page
page.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
width, height = page.evaluate <<~JS
  [document.documentElement.offsetWidth,
   document.documentElement.offsetHeight]
JS
# => [1024, 1931]
browser.quit
```

Do any mouse movements you like:

```ruby
# Trace a 100x100 square
browser = Ferrum::Browser.new
page = browser.create_page
page.go_to("https://google.com")
page.mouse
    .move(x: 0, y: 0)
    .down
    .move(x: 0, y: 100)
    .move(x: 100, y: 100)
    .move(x: 100, y: 0)
    .move(x: 0, y: 0)
    .up

browser.quit
```

## Clean Up

Closes browser tabs opened by the `Browser` instance.

```ruby
# connect to a long-running Chrome process
browser = Ferrum::Browser.new(url: "http://localhost:9222")

browser.go_to("https://github.com/")

# clean up, lest the tab stays there hanging forever
browser.reset

browser.quit
```

`#quit` blocks by default until the browser process is confirmed dead and its user data directory removed. Killing a
stubborn process (one that ignores `TERM` and needs `KILL`) can take a couple of seconds, which matters if you're
quitting many browsers in a hot path. Pass `wait: false` to return immediately and run that cleanup on a background
thread instead:

```ruby
browser = Ferrum::Browser.new
thread = browser.quit(wait: false)
# ... do other work while the browser is killed and its directory removed in the background ...
thread.join # only needed if you must wait for cleanup to finish, e.g. before reusing a fixed port
```

## Thread safety

Ferrum is fully thread-safe. You can create one browser or a few as you wish and
start playing around using threads. Example below shows how to create a few pages
which share the same context. Context is similar to an incognito profile but you
can have more than one, think of it like it's independent browser session:

```ruby
browser = Ferrum::Browser.new
context = browser.contexts.create

t1 = Thread.new(context) do |c|
  page = c.create_page
  page.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
  page.screenshot(path: "t1.png")
end

t2 = Thread.new(context) do |c|
  page = c.create_page
  page.go_to("https://www.google.com/search?q=Ruby+static+typing")
  page.screenshot(path: "t2.png")
end

t1.join
t2.join

context.dispose
browser.quit
```

or you can create two independent contexts:

```ruby
browser = Ferrum::Browser.new

t1 = Thread.new(browser) do |b|
  context = b.contexts.create
  page = context.create_page
  page.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
  page.screenshot(path: "t1.png")
  context.dispose
end

t2 = Thread.new(browser) do |b|
  context = b.contexts.create
  page = context.create_page
  page.go_to("https://www.google.com/search?q=Ruby+static+typing")
  page.screenshot(path: "t2.png")
  context.dispose
end

t1.join
t2.join

browser.quit
```

## Development

After checking out the repo, run `bundle install` to install dependencies.

Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will
allow you to experiment.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).


## Contributing

Bug reports and pull requests are welcome on [GitHub](https://github.com/rubycdp/ferrum).

---

### 10 Mouse Keyboard (`docs/10-mouse-keyboard.md`)

---
sidebar_position: 10
---

# Mouse & Keyboard

## Mouse

`page.mouse`

#### scroll_to(x, y)

Scroll page to a given x, y

* x `Integer` the pixel along the horizontal axis of the document that you
  want displayed in the upper left
* y `Integer` the pixel along the vertical axis of the document that you want
  displayed in the upper left

```ruby
page.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
page.mouse.scroll_to(0, 400)
```

#### click(\*\*options) : `Mouse`

Click given coordinates, fires mouse move, down and up events.

* options `Hash`
    * :x `Integer`
    * :y `Integer`
    * :delay `Float` defaults to 0. Delay between mouse down and mouse up events
    * :button `Symbol` :left | :right, defaults to :left
    * :count `Integer` defaults to 1
    * :modifiers `Integer` bitfield for key modifiers. See `keyboard.modifiers`.
      If omitted, uses the keys held by `keyboard.hold`, or none outside of it

#### down(\*\*options) : `Mouse`

Mouse down for given coordinates.

* options `Hash`
    * :button `Symbol` :left | :right, defaults to :left
    * :count `Integer` defaults to 1
    * :modifiers `Integer` bitfield for key modifiers. See `keyboard.modifiers`.
      If omitted, uses the keys held by `keyboard.hold`, or none outside of it

#### up(\*\*options) : `Mouse`

Mouse up for given coordinates.

* options `Hash`
    * :button `Symbol` :left | :right, defaults to :left
    * :count `Integer` defaults to 1
    * :modifiers `Integer` bitfield for key modifiers. See `keyboard.modifiers`.
      If omitted, uses the keys held by `keyboard.hold`, or none outside of it

#### move(x:, y:, steps: 1, modifiers: nil) : `Mouse`

Mouse move to given x and y.

* options `Hash`
    * :x `Integer`
    * :y `Integer`
    * :steps `Integer` defaults to 1. Sends intermediate mousemove events.
    * :modifiers `Integer` bitfield for key modifiers. See `keyboard.modifiers`.
      If omitted, uses the keys held by `keyboard.hold`, or none outside of it

## Keyboard

`page.keyboard`

#### down(key) : `Keyboard`

Dispatches a keydown event.

* key `String` | `Symbol` Name of key such as "a", :enter, :backspace. Raises `ArgumentError` for a modifier,
  use `hold` instead

#### up(key) : `Keyboard`

Dispatches a keyup event.

* key `String` | `Symbol` Name of key such as "b", :enter, :backspace. Raises `ArgumentError` for a modifier,
  use `hold` instead

#### type(\*keys) : `Keyboard`

Sends a keydown, keypress/input, and keyup event for each character in the text.

* text `String` | `Array<String> | Array<Symbol>` A text to type into a focused
  element, `[:Shift, "s"], "tring"`

#### hold(\*keys) { } : `Object`

Holds modifier keys down while the block runs. Every keyboard and mouse event inside the block carries them, and they
are released when the block returns or raises. Returns the block's value.

* keys `Symbol` :alt | :ctrl | :control | :meta | :command | :shift

```ruby
page.keyboard.hold(:shift) do
  page.mouse.move(x: 10, y: 10).down.move(x: 200, y: 200, steps: 10).up
end
```

#### held_modifiers : `Integer`

Modifier keys currently pressed by `hold`, in the same format as `modifiers` returns. It's 0 outside a `hold` block.

```ruby
page.keyboard.hold(:shift) { page.keyboard.held_modifiers } # => 8
```

#### modifiers(keys) : `Integer`

Returns bitfield for a given keys

* keys `Array<Symbol>` :alt | :ctrl | :command | :shift

## Examples

```ruby
# Type text into an input
page.go_to("https://google.com")
input = page.at_css("input[name='q']")
input.focus
page.keyboard.type("Hello World")

# Press Enter
page.keyboard.type(:Enter)

# Use keyboard shortcuts
page.keyboard.hold(:shift) { page.keyboard.type("hello") }
```

---

### 11 Cookies (`docs/11-cookies.md`)

---
sidebar_position: 11
---

# Cookies

`page.cookies`

#### all : `Hash<String, Cookie>`

Returns cookies hash

```ruby
page.cookies.all # => {"NID"=>#<Ferrum::Cookies::Cookie:0x0000558624b37a40 @attributes={"name"=>"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/", "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false}>}
```

#### \[\](value) : `Cookie`

Returns cookie

* value `String`

```ruby
page.cookies["NID"] # => <Ferrum::Cookies::Cookie:0x0000558624b67a88 @attributes={"name"=>"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/", "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false}>
```

#### set(value) : `Boolean`

Sets a cookie

* value `Hash`
  * :name `String`
  * :value `String`
  * :domain `String`
  * :expires `Integer`
  * :samesite `String`
  * :httponly `Boolean`

```ruby
page.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true
```

* value `Cookie`

```ruby
nid_cookie = page.cookies["NID"] # => <Ferrum::Cookies::Cookie:0x0000558624b67a88>
page.cookies.set(nid_cookie) # => true
```

#### remove(\*\*options) : `Boolean`

Removes given cookie

* options `Hash`
  * :name `String`
  * :domain `String`
  * :url `String`

```ruby
page.cookies.remove(name: "stealth", domain: "google.com") # => true
```

#### clear : `Boolean`

Removes all cookies for current page

```ruby
page.cookies.clear # => true
```

#### store(path) : `Boolean`

Stores all cookies of current page in a file.

```ruby
# Cookies are saved into cookies.yml
page.cookies.store # => 15657
```

#### load(path) : `Boolean`

Loads all cookies from the file and sets them for current page.

```ruby
# Cookies are loaded from cookies.yml
page.cookies.load # => true
```

---

### 12 Headers (`docs/12-headers.md`)

---
sidebar_position: 12
---

# Headers

`page.headers`

#### get : `Hash`

Get all headers

#### set(headers) : `Boolean`

Set given headers. Eventually clear all headers and set given ones.

* headers `Hash` key-value pairs for example `"User-Agent" => "Browser"`

#### add(headers) : `Boolean`

Adds given headers to already set ones.

* headers `Hash` key-value pairs for example `"Referer" => "http://example.com"`

#### clear : `Boolean`

Clear all headers.

---

### 13 Javascript (`docs/13-javascript.md`)

---
sidebar_position: 13
---

# JavaScript

#### evaluate(expression, \*args, timeout: nil, \*\*named)

Evaluate JavaScript and return the result, converted to Ruby.

* expression `String` JavaScript returning the value of its last statement, or a function/arrow declaration, which
  is used as-is
* args positional arguments, a declaration's parameters or `arguments[0]` and up. Can't be mixed with named ones
* timeout `Numeric` seconds to wait for the script and any promise it returns, defaults to the page timeout. Raises
  `Ferrum::ScriptTimeoutError` when exceeded, which inherits from `Ferrum::TimeoutError`
* named named arguments, which become the parameters of a script that isn't a declaration. A `Ferrum::Node` arrives
  as the live element, but can't be put inside an Array or Hash. `timeout` can't be a parameter name

```ruby
page.evaluate("[window.scrollX, window.scrollY]") # => [0, 0]
page.evaluate("a + b", a: 1, b: 2) # => 3
page.evaluate("el.getAttribute(name)", el: page.at_css("a"), name: "href")
page.evaluate("await fetch(url).then(r => r.status)", url: "/") # => 200
page.evaluate(<<~JS, 3) # => 9
  function(c) {
    const sum = 1 + 2;
    return sum * c;
  }
JS
```

The result is converted to Ruby: objects become `Hash`, arrays and node lists `Array`, DOM nodes `Ferrum::Node`,
dates ISO 8601 strings, `NaN` and `Infinity` `Float`, `BigInt` `Integer`, `null` and `undefined` `nil`. Values
without a Ruby counterpart, such as functions, `Map`, `Set` or `window`, become `{}`. A reference back to an
enclosing object, e.g. `a.self = a`, becomes `Ferrum::CyclicObject`.

#### evaluate_handle(expression, \*args, timeout: nil, \*\*named) : `Ferrum::RemoteObject`

Same as `evaluate`, but keeps the value in the browser and returns a reference you can pass back in as an argument.
Call `#value` to convert it to Ruby, and `#release` to free it.

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

#### execute(expression, \*args, timeout: nil, \*\*named) : `Boolean`

Run JavaScript for its side effects. The script is used as a function body, so statements need no wrapping.

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

`Ferrum::Node` has the same three methods, with `this` bound to the element. Arrow functions don't get `this`, so use a
`function` declaration when you need it.

```ruby
page.at_css("input").evaluate("this.value")
page.at_css("input").execute("this.value = text", text: "hello")
```

#### Upgrading to 0.21

Scripts written for the old API run exactly as before, and Ferrum warns, pointing at your code, about any whose result
will change. Calls that only the new API can make, e.g. with named arguments, already run the new way.

* 0.20 runs old-style scripts the old way and warns. Set `FERRUM_JS_API=new` to run everything the new way.
* 0.21 removes the old way, along with `evaluate_async`, `evaluate_func` and `evaluate_on`. There's no switch back.

Warnings only cover the scripts and results your suite actually runs into, so before upgrading to 0.21, run the suite
once with `FERRUM_JS_API=new`. Set `FERRUM_DEPRECATION_WARNINGS=raise` to turn the warnings into errors while
migrating, or `0` to silence them. A library built on Ferrum should pass named arguments or a function declaration,
which run the new way in both 0.20 and 0.21.

| Script | < 0.21 | >= 0.21 | Notes |
|---|---|---|---|
| `evaluate("arguments[0].foo", foo: 1)` | `1` | `nil` | keywords are named parameters now, write `evaluate("foo", foo: 1)` |
| `evaluate("[arguments[0], arguments[1]]", 1, foo: 2)` | `[1, { "foo" => 2 }]` | `ArgumentError` | positional and keyword arguments can't be mixed, pass `{ foo: 2 }` in braces |
| `evaluate("a(); b")` | `a()`'s value, `b` never runs | `b`'s value | every statement runs, the last one's value is returned |
| `evaluate("\n  window.ran = true\n")` | `nil`, nothing runs | `true` | the line break no longer ends the `return`, check it's safe to run |
| `evaluate("Promise.resolve(1)")` | `{}` | `1` | promises are awaited, `void` keeps one from being awaited |
| `node.evaluate("this.parentNode")` | `{}` | `Ferrum::Node` | |
| `node.evaluate("new Date(0)")` | `{}` | `"1970-01-01T00:00:00.000Z"` | |
| `evaluate_async("arguments[0](1)", 5)` | `1` | `NoMethodError` | `evaluate("await …", timeout: 5)` |
| `evaluate_func("function(a) { return a }", 1)` | `1` | `NoMethodError` | `evaluate("function(a) { return a }", 1)`, with `on:` use `node.evaluate` |
| `evaluate_on(node: node, expression: "this.id")` | `"main"` | `NoMethodError` | `node.evaluate("this.id")` |
| `evaluate_on(node: link, expression: "this.click()", wait: 0.1)` | waits for the new page | `NoMethodError` | `page.wait_for_navigation!(0.1) { link.evaluate("this.click()") }` |

#### evaluate_on_new_document(source, \*\*options) : `String`

Runs JavaScript in every new document of the page and its in-process frames, before the document's own scripts.
Returns the script identifier. For every page and iframe of the browser, use `add_extension`.

* source `String` should be valid JavaScript
* options `Hash`
  * :world_name `String` - run in an isolated world with this name instead of the page's main world
  * :run_immediately `Boolean` - run in the current document as well, `false` by default

```ruby
id = page.evaluate_on_new_document <<~JS
  Object.defineProperty(navigator, "languages", {
    get: function() { return ["tlh"]; }
  });
JS
```

#### isolated_world(name) : `IsolatedWorld`

Returns the frame's isolated world with this name, its own JavaScript globals over the frame's DOM, so the page's
scripts can't see or override what runs there. It has `evaluate`, `evaluate_handle` and `execute`,
takes and returns nodes, and comes back empty in every new document. On a page it's the main frame's world, use
`frame.isolated_world(name)` for an iframe. It's the same world `evaluate_on_new_document` runs in with
`world_name:`.

* name `String`

```ruby
world = page.isolated_world("ferrum")
world.execute("window.secret = 'top'")
world.evaluate("window.secret") # => "top"
page.evaluate("window.secret") # => nil
world.evaluate("document.title") # => same as page.evaluate("document.title")
```

#### remove_script_on_new_document(identifier) : `Boolean`

Stops running a script added with `evaluate_on_new_document` in new documents.

```ruby
page.remove_script_on_new_document(id) # => true
```

#### add_extension(extension)

Adds an extension, see the `:extensions` option. It runs in every page and iframe, popups and out-of-process iframes
included, attached after the call, before their own scripts. Pages already open keep the extensions they had.

* extension `String | Hash` a path to a JS file or `{ source: "..." }`

```ruby
browser.add_extension("/path/to/stub.js")
browser.add_extension(source: "window.secret = 'top'")
```

#### add_script_tag(\*\*options) : `Boolean`

* options `Hash`
  * :url `String`
  * :path `String`
  * :content `String`
  * :type `String` - `text/javascript` by default

```ruby
page.add_script_tag(url: "http://example.com/stylesheet.css") # => true
```

#### add_style_tag(\*\*options) : `Boolean`

* options `Hash`
  * :url `String`
  * :path `String`
  * :content `String`

```ruby
page.add_style_tag(content: "h1 { font-size: 40px; }") # => true

```
#### bypass_csp(\*\*options) : `Boolean`

* options `Hash`
  * :enabled `Boolean`, `true` by default

```ruby
page.bypass_csp # => true
page.go_to("https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/promises.in.md")
page.refresh
page.add_script_tag(content: "window.__injected = 42")
page.evaluate("window.__injected") # => 42
```

#### disable_javascript

Disables Javascripts from the loaded HTML source.
You can still evaluate JavaScript with `evaluate` or `execute`.
Returns nothing.

```ruby
page.disable_javascript
```

## Console

#### console : `Console`

What the DevTools console would show for the page: `console.*` calls, browser log entries (failed resource loads,
CSP violations, deprecations, ...) and uncaught exceptions. It keeps the last 1000 messages, oldest first, and is
`Enumerable`, so filter it like any collection.

```ruby
page.go_to("https://example.com")
page.console.select(&:error?) # => [#<Ferrum::Console::Message type=:error source=:network ...>]
page.console.select { |message| message.match?(/deprecated/) }
page.console.last.text # => "Hello world"
page.console.clear
```

`Ferrum::Console::Message` has:

* `type` `Symbol` - `:log`, `:info`, `:warning`, `:error`, `:debug`, `:table`, ... for `console.*` calls, the log
  entry's level for browser log entries, `:error` for uncaught exceptions
* `source` `Symbol` - `:console_api`, `:exception`, or the log entry's source such as `:network` or `:security`
* `text` `String` - the arguments joined by spaces, objects and arrays as JSON
* `args` `Array` - the `console.*` arguments as Ruby values, objects and arrays built from Chrome's shallow preview
* `location` `Location` and `stack_trace` `Array<Location>` - `url`, `line`, `column`, `function_name`
* `timestamp` `Time`
* `severity` `Symbol` - `:debug`, `:info`, `:warn` or `:error`, the matching `Logger` method
* `error?`, `warning?`, `match?(pattern)`

#### on(:console)

Calls the block for every new message.

```ruby
logger = Logger.new($stdout)
page.on(:console) { |message| logger.public_send(message.severity, message.to_s) }
```

Fail a test on browser errors:

```ruby
after { expect(browser.console.select(&:error?)).to be_empty }
```

---

### 14 Emulation (`docs/14-emulation.md`)

---
sidebar_position: 14
---

# Emulation

#### set_viewport

Overrides device screen dimensions and emulates viewport. When `:mobile` is
`true`, this also enables touch emulation (`navigator.maxTouchPoints`,
`ontouchstart`) alongside the mobile viewport metrics.

* options `Hash`
  * :width `Integer`, viewport width. `0` by default
  * :height `Integer`, viewport height. `0` by default
  * :scale_factor `Float`, device scale factor. `0` by default
  * :mobile `Boolean`, whether to emulate mobile device and enable touch. `false` by default

```ruby
page.set_viewport(width: 1000, height: 600, scale_factor: 3)
```

`:width`, `:height`, and `:scale_factor` all default to `0`, which per the
[DevTools protocol](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride)
disables overriding that particular value. This means `mobile:` can be
enabled on its own, at the page's current size, without specifying a device
size preset:

```ruby
page.set_viewport(mobile: true)
```

---

### 15 Frames (`docs/15-frames.md`)

---
sidebar_position: 15
---

# Frames

Frames allow you to work with iframes and nested documents within a page. Each frame has its own execution context and can be interacted with independently.

#### frames : `Array[Frame] | []`

Returns all the frames current page have.

```ruby
page.go_to("https://www.w3schools.com/tags/tag_frame.asp")
page.frames # =>
# [
#   #<Ferrum::Frame @id="C6D104CE454A025FBCF22B98DE612B12" @parent_id=nil @name=nil @state=:stopped_loading @execution_id=1>,
#   #<Ferrum::Frame @id="C09C4E4404314AAEAE85928EAC109A93" @parent_id="C6D104CE454A025FBCF22B98DE612B12" @state=:stopped_loading @execution_id=2>,
#   #<Ferrum::Frame @id="2E9C7F476ED09D87A42F2FEE3C6FBC3C" @parent_id="C6D104CE454A025FBCF22B98DE612B12" @state=:stopped_loading @execution_id=3>,
#   ...
# ]
```

#### main_frame : `Frame`

Returns page's main frame, the top of the tree and the parent of all frames.

#### frame_by(\*\*options) : `Frame | nil`

Find frame by given options.

* options `Hash`
    * :id `String` - Unique frame's id that browser provides
    * :name `String` - Frame's name if there's one

```ruby
page.frame_by(id: "C6D104CE454A025FBCF22B98DE612B12")
```

## Frame

## Frame

#### id : `String`

Frame's unique id.

#### parent_id : `String | nil`

Parent frame id if this one is nested in another one.

#### parent : `Frame | nil`

Parent frame if this one is nested in another one.

#### frame_element : `Node | nil`

Returns the element in which the window is embedded. It's `nil` for the main
frame and for a frame from a different origin than its parent, since the
same-origin policy hides `window.frameElement` from a cross-origin frame.

#### execution_id : `Integer`

Execution context id which is used by JS, each frame has its own context in
which JS evaluates.

#### name : `String | nil`

If frame was given a name it should be here.

#### state : `Symbol | nil`

One of the states frame's in:

* `:started_loading`
* `:navigated`
* `:stopped_loading`

#### loader_id : `String | nil`

Id of the loader associated with the frame's current navigation, set once the frame navigates.

#### lifecycle_events : `Array[Hash]`

List of the frame's lifecycle events as reported by the browser, each a `{"name" => String, "timestamp" => Float}`
hash, e.g. `init`, `networkIdle`, `firstPaint`.

#### idle? : `Boolean`

Whether the frame has finished loading (`:stopped_loading` state). Frames in `:canceled` state (execution context
torn down mid-navigation) are not considered idle.

```ruby
page.go_to("https://example.com")
page.main_frame.idle? # => true
```

#### url : `String`

Returns current frame's location href.

```ruby
page.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
frame = page.frames[1]
frame.url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
```

#### title

Returns current frame's title.

```ruby
page.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
frame = page.frames[1]
frame.title # => HTML Demo: <iframe>
```

#### main? : `Boolean`

If current frame is the main frame of the page (top of the tree).

```ruby
page.go_to("https://www.w3schools.com/tags/tag_frame.asp")
frame = page.frame_by(id: "C09C4E4404314AAEAE85928EAC109A93")
frame.main? # => false
```

#### current_url : `String`

Returns current frame's top window location href.

```ruby
page.go_to("https://www.w3schools.com/tags/tag_frame.asp")
frame = page.frame_by(id: "C09C4E4404314AAEAE85928EAC109A93")
frame.current_url # => "https://www.w3schools.com/tags/tag_frame.asp"
```

#### current_title : `String`

Returns current frame's top window title.

```ruby
page.go_to("https://www.w3schools.com/tags/tag_frame.asp")
frame = page.frame_by(id: "C09C4E4404314AAEAE85928EAC109A93")
frame.current_title # => "HTML frame tag"
```

#### body : `String`

Returns current frame's html.

```ruby
page.go_to("https://www.w3schools.com/tags/tag_frame.asp")
frame = page.frame_by(id: "C09C4E4404314AAEAE85928EAC109A93")
frame.body # => "<html><head></head><body></body></html>"
```

#### doctype

Returns current frame's doctype.

```ruby
page.go_to("https://www.w3schools.com/tags/tag_frame.asp")
page.main_frame.doctype # => "<!DOCTYPE html>"
```

#### content = html

Sets a content of a given frame.

* html `String`

```ruby
page.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
frame = page.frames[1]
frame.body # <html lang="en"><head><style>body {transition: opacity ease-in 0.2s; }...
frame.content = "<html><head></head><body><p>lol</p></body></html>"
frame.body # => <html><head></head><body><p>lol</p></body></html>
```

## Example

You can access a frame and then use finders to locate elements within that frame:

```ruby
page.go_to("https://example.com/page-with-iframe")
frame = page.at_xpath("//iframe").frame # => Frame
frame.at_css("//a[text() = 'Log in']") # => Node
```

---

### 16 Dialogs (`docs/16-dialogs.md`)

---
sidebar_position: 16
---

# Dialogs

#### accept(text)

Accept dialog with given text or default prompt if applicable

* text `String`

#### dismiss

Dismiss dialog

```ruby
page.on(:dialog) do |dialog|
  if dialog.match?(/bla-bla/)
    dialog.accept
  else
    dialog.dismiss
  end
end
page.go_to("https://google.com")
```

---

### 17 Animation (`docs/17-animation.md`)

---
sidebar_position: 17
---

# Animation

You can slow down or speed up CSS animations.

#### playback_rate : `Integer`

Returns playback rate for CSS animations, defaults to `1`.


#### playback_rate = value

Sets playback rate of CSS animations

* value `Integer`

```ruby
page.playback_rate = 2000
page.go_to("https://google.com")
page.playback_rate # => 2000
```

---

### 18 Node (`docs/18-node.md`)

---
sidebar_position: 18
---

# Node

#### node? : `Boolean`
#### frame_id
#### frame  : `Frame`

Returns [Frame](https://github.com/rubycdp/ferrum#frame) object for current node, you can keep using
[Finders](https://github.com/rubycdp/ferrum#Finders) for that object:

```ruby
frame =  page.at_xpath("//iframe").frame # => Frame
frame.at_css("//a[text() = 'Log in']") # => Node
```

#### focus
#### focusable?
#### moving? : `Boolean`
#### wait_for_stop_moving
#### blur
#### type
#### click
#### hover
#### select_file
#### at_xpath
#### at_css
#### xpath
#### css
#### text
#### inner_text
#### value
#### property
#### attribute
#### evaluate
#### selected : `Array<Node>`
#### select
#### scroll_into_view
#### in_viewport?(of: `Node | nil`) : `Boolean`
#### remove
#### exists?

(chainable) Selects options by passed attribute.

```ruby
page.at_xpath("//*[select]").select(["1"]) # => Node (select)
page.at_xpath("//*[select]").select(["text"], by: :text) # => Node (select)
```

Accept string, array or strings:
```ruby
page.at_xpath("//*[select]").select("1")
page.at_xpath("//*[select]").select("1", "2")
page.at_xpath("//*[select]").select(["1", "2"])
```

---

### 19 Tracing (`docs/19-tracing.md`)

---
sidebar_position: 19
---

# Tracing

You can use `tracing.record` to create a trace file which can be opened in Chrome DevTools or
[timeline viewer](https://chromedevtools.github.io/timeline-viewer/).

```ruby
page.tracing.record(path: "trace.json") do
  page.go_to("https://www.google.com")
end
```

#### tracing.record(\*\*options) : `String`

Accepts block, records trace and by default returns trace data from `Tracing.tracingComplete` event as output. When
`path` is specified returns `true` and stores trace data into file.

* options `Hash`
    * :path `String` save data on the disk, `nil` by default
    * :encoding `Symbol` `:base64` | `:binary` encode output as Base64 or plain text. `:binary` by default
    * :timeout `Float` wait until file streaming finishes in the specified time or raise error, defaults to `nil`
    * :screenshots `Boolean` capture screenshots in the trace, `false` by default
    * :trace_config `Hash<String, Object>` config for
      [trace](https://chromedevtools.github.io/devtools-protocol/tot/Tracing/#type-TraceConfig), for categories
      see [getCategories](https://chromedevtools.github.io/devtools-protocol/tot/Tracing/#method-getCategories),
      only one trace config can be active at a time per browser.

---

### 2 Customization (`docs/2-customization.md`)

---
sidebar_position: 2
---

# Customization

You can customize options with the following code in your test setup:

``` ruby
Ferrum::Browser.new(options)
```

* options `Hash`
    * `:headless` (Boolean) - Set browser as headless or not, `true` by default.
    * `:incognito` (Boolean) - Create an incognito profile for the browser startup window, `true` by default.
    * `:dockerize` (Boolean) - Provide CLI flags to the browser to run it in a container, `false` by default.
    * `:xvfb` (Boolean) - Run browser in a virtual framebuffer, `false` by default.
    * `:flatten` (Boolean) - Use one websocket connection to the browser and all the pages in flatten mode,
      `true` by default. When set to `false`, each page/target opens its own dedicated websocket connection instead
      of sharing the browser's connection.
    * `:window_size` (Array) - The dimensions of the browser window in which to
      test, expressed as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
    * `:extensions` (Array[String | Hash]) - An array of paths to files or JS
      source code to run in every page and iframe before it loads, e.g.:
      `["/path/to/script.js", { source: "window.secret = 'top'" }]`. Files are read in `Browser.new`.
    * `:logger` (Object responding to `puts`, or a `Logger`) - When present, debug output is
      written to it, see [Logging](#logging).
    * `:slowmo` (Integer | Float) - Set a delay in seconds to wait before sending command.
      Useful companion of headless option, so that you have time to see changes.
    * `:timeout` (Numeric) - The number of seconds we'll wait for a response when
      communicating with browser. Default is 5.
    * `:js_errors` (Boolean) - When true, JavaScript errors get re-raised in Ruby.
    * `:pending_connection_errors` (Boolean) - Raise `PendingConnectionsError` when main frame is still waiting
      for slow responses and timeout is reached. Default is false.
    * `:browser_name` (Symbol) - `:chrome` by default, `:opera` and `:edge` are
      also supported.
    * `:browser_path` (String) - Path to Chrome binary, you can also set ENV
      variable as `BROWSER_PATH=some/path/chrome bundle exec rspec`.
    * `:browser_options` (Hash) - Additional command line options,
      [see them all](https://peter.sh/experiments/chromium-command-line-switches/)
      e.g. `{ "ignore-certificate-errors" => nil }`
    * `:ignore_default_browser_options` (Boolean) - Ferrum has a number of default
      options it passes to the browser, if you set this to `true` then only
      options you put in `:browser_options` will be passed to the browser,
      except required ones of course.
    * `:port` (Integer) - Remote debugging port for headless Chrome.
    * `:host` (String) - Host we communicate with when spawning browser, `127.0.0.1` by default.
      Chrome always listens on `127.0.0.1` regardless of this option, so the host must resolve to `127.0.0.1` for
      Ferrum to actually be able to connect.
    * `:url` (String) - URL for a running instance of Chrome. If this is set, a
      browser process will not be spawned.
    * `:ws_url` (String) - Websocket url for a running instance of Chrome. If this is set, a
      browser process will not be spawned. It's higher priority than `:url`, setting both doesn't make sense.
    * `:process_timeout` (Integer) - How long to wait for the Chrome process to
      respond on startup.
    * `:ws_max_receive_size` (Integer) - How big messages to accept from Chrome
      over the web socket, in bytes. Defaults to 64MB. Incoming messages larger
      than this will cause a `Ferrum::DeadBrowserError`.
    * `:proxy` (Hash) - Specify proxy settings, [read more](https://github.com/rubycdp/ferrum#proxy)
    * `:save_path` (String) - Path to save attachments with [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header.
    * `:env` (Hash) - Environment variables you'd like to pass through to the process

## Logging

With `:logger`, or `FERRUM_LOG` set to print to `$stdout`, Ferrum writes two channels:

* `protocol` - every CDP message sent (`▶`) and received (`◀`). String values longer than 1024 characters, such as
  Base64 screenshots, are cut; set `FERRUM_LOG_VALUE_LENGTH` to another length, or to `0` to keep them whole.
* `browser` - what the Chrome process Ferrum launched writes to stdout and stderr.

`FERRUM_LOG` picks what's written, as a comma-separated list:

```sh
FERRUM_LOG=*                         # everything, what to attach to a bug report
FERRUM_LOG=*,-browser                # everything but Chrome's output
FERRUM_LOG=*,-Network.*,-Target.*    # leave out CDP messages whose method matches
FERRUM_LOG=Page.*,Runtime.evaluate   # only CDP messages whose method matches
FERRUM_LOG=0                         # nothing, even with :logger
```

Items with a dot are CDP method patterns, a leading `-` leaves things out and wins over the rest, and a response is
kept or dropped with the command it answers. Without `FERRUM_LOG`, `:logger` gets everything. `FERRUM_DEBUG` still
works as `FERRUM_LOG=*`, but is deprecated.

`FERRUM_LOG_FORMAT` picks how CDP messages look. `json` writes each message as JSON alone, one per line and never
colored, for tools such as `jq`. `annotated` puts the command id and method in front of each CDP message, one line
each, and gives a response the method of its command and how long the answer took. Params, results and events stay
whole JSON:

```
▶ 1.623 #23 Page.navigate {"url":"http://127.0.0.1:5000/console_log"} @4E470240
    ◀ 1.637 #23 Page.navigate 14ms {"frameId":"325D8B09…","loaderId":"64738D2F…"} @4E470240
    ◀ 1.640 Runtime.consoleAPICalled {"type":"warning","args":[{"type":"string","value":"hey"}],…} @4E470240
```

Without `FERRUM_LOG_FORMAT`, lines look as they always have, `▶ 1.623 {"id":23,"method":"Page.navigate",...}`.

Written to a terminal, lines are colored with the basic ANSI colors, which follow the terminal's light or dark theme:
arrows are colored by kind (sent, response, error, event), methods are bold and times dimmed. The text stays the
same, and files, pipes, `StringIO` and `Logger`s get it without colors, as does a terminal when `NO_COLOR` is set.
`FERRUM_LOG_COLOR=always` or `never` overrides both.

A `Logger` gets every line at debug level, with `ferrum:protocol` or `ferrum:browser` as program name, and nothing is
built when its level is higher.

Console messages aren't logged, subscribe to them with [`page.on(:console)`](/docs/ferrum/javascript#console) and
write them wherever you like.

Chrome's output is also kept when nothing is logged: `browser.process.output.lines` holds its last 100 lines, and
`ProcessTimeoutError#output` has them when Chrome doesn't start.

## The crashpad handler

Chrome starts two `chrome_crashpad_handler` processes per browser on Linux. Their only job is to collect and upload
crash reports, which no automated browser has any use for, but **Ferrum does not disable them**, because there is no
safe way to.

Handlers Chrome starts on Linux:

| Chrome | `--headless` | `--headless=new` | headful |
|---|---|---|---|
| 127 and older | 0 | 2 | 2 |
| 128 and newer | 2 | 2 | 2 |

New headless always starts them. What changed in 128 is what bare `--headless` *means*: before it, that selected old
headless, a separate lightweight shell with no crash handler; from 128 on it selects new headless, which is full
Chrome. Headful has always behaved like new headless.

### No flag turns them off safely

| Flag | What it does |
|---|---|
| `--disable-breakpad` | Stops crash *reporting*, not the handler process |
| `--disable-crash-reporter` | Same |
| `--no-crashpad` | Not a Chromium switch at all, so Chrome ignores it |
| `--disable-crashpad-for-testing` | Real, and it does stop them — but it breaks the browser |

:::danger
Do not pass `--disable-crashpad-for-testing`. It is named `for-testing` because it is meant for Chromium's own test
harness, where the process tree is fully controlled. In a normally launched browser, child processes die at startup
with `Crashing due to FD ownership violation`, and the network service crash-loops.

The failure is near-invisible: the browser process survives, so it starts normally and CDP answers every command.
There is simply no network service behind it, so every navigation returns `net::ERR_ABORTED` and the document stays
`about:blank`.
:::

### Operating system differences

On macOS one handler starts rather than two, and it exits when the browser does, so nothing accumulates.

It is also invisible to `pstree`, because it double-forks and reparents to launchd and is therefore never a
descendant of the process that started Chrome. Look for it globally instead:

```console
$ ps -Ao pid=,ppid=,comm= | grep crashpad
13270     1 .../Helpers/chrome_crashpad_handler
```

`ppid` is `1`, and the pid usually lands just after Chrome's own.

### Why this matters most in Docker

The handler is harmless outside a container. It is not in Chrome's process group, so the signal Ferrum sends on
teardown never reaches it — and does not need to: it watches the browser and exits by itself once Chrome is gone.
What is left behind is an exit status, and because the handler double-forks away from Chrome its parent is pid 1.
Collecting it is PID 1's job which doesn't exist in a container, so the handlers pile up as `<defunct>` entries.
See [Docker](/docs/ferrum/docker).

## Examples

```ruby
# Run in headful mode with custom window size
Ferrum::Browser.new(headless: false, window_size: [1920, 1080])

# Connect to an existing Chrome instance
Ferrum::Browser.new(url: "http://localhost:9222")

# Enable JavaScript error raising
Ferrum::Browser.new(js_errors: true)

# Set custom timeout and slowmo for debugging
Ferrum::Browser.new(timeout: 10, slowmo: 0.5)

# Use custom Chrome binary
Ferrum::Browser.new(browser_path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")

# Add custom browser options
Ferrum::Browser.new(browser_options: { "disable-web-security" => nil })
```

---

### 20 Accessibility (`docs/20-accessibility.md`)

---
sidebar_position: 20
---

# Accessibility

Ferrum wraps the CDP [Accessibility](https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/) domain,
letting you read the accessibility (AX) tree that assistive technologies (like screen readers) rely on. The query
methods below work without calling `enable` first; `enable`/`disable` exist only if you want live AX events.

`page.accessibility` returns the `Ferrum::Accessibility` instance for a page.

:::note
The node-scoped methods (`node_for`, `partial_tree`, and `query` with a `node:`) issue their command against the
node's owning page session. They support same-process (same-target) iframes; nodes living in an out-of-process
iframe (OOPIF, a separate CDP target) are not resolvable and will error or return an empty result.
:::

#### node_for(node) : `AXNode | nil`

Returns the single non-ignored `AXNode` for a DOM node, or `nil`.

* node `Ferrum::Node`

```ruby
node = page.at_css("button")
page.accessibility.node_for(node) # => #<Ferrum::Accessibility::AXNode ...>
```

You can also get to it directly from the node itself:

#### axnode : `AXNode | nil`

`Node#axnode` is a shortcut for `page.accessibility.node_for(self)`.

```ruby
page.at_css("button").axnode.role # => "button"
```

#### partial_tree(\*\*options) : `Array[AXNode]`

Returns the partial AX tree for a DOM node.

* options `Hash`
    * :node `Ferrum::Node` **required**
    * :fetch_relatives `Boolean` whether to include related nodes, `false` by default

```ruby
node = page.at_css("form")
page.accessibility.partial_tree(node: node)
```

#### snapshot(\*\*options) : `Array[AXNode]`

Returns the full AX tree for the page.

* options `Hash`
    * :depth `Integer` how many levels deep to fetch, unlimited by default
    * :frame_id `String` restrict the snapshot to a given frame

```ruby
page.accessibility.snapshot
```

#### root(\*\*options) : `AXNode | nil`

Returns the root `AXNode` of the (optionally framed) document.

* options `Hash`
    * :frame_id `String` restrict to a given frame

```ruby
page.accessibility.root
```

#### query(\*\*options) : `Array[AXNode]`

Query the AX tree by accessible name and/or role.

* options `Hash`
    * :name `String` accessible name to match
    * :role `String` AX role to match, e.g. `"button"`
    * :node `Ferrum::Node` scope the query to this node's subtree, whole page by default

```ruby
page.accessibility.query(role: "button")
page.accessibility.query(name: "Submit", role: "button")
```

#### enable : `self`
#### disable : `self`

Enable/disable the CDP Accessibility domain, only needed if you want live AX events; the query methods above work
without it.

## AXNode

Represents a single [AXNode](https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/#type-AXNode)
from the CDP Accessibility domain. Instances are returned by the methods above, never constructed directly.

#### role : `String | nil`

The AX role, e.g. `"button"`, `"heading"`.

#### name : `String | nil`

The accessible name.

#### description : `String | nil`

The accessible description.

#### value : `String | Numeric | Boolean | nil`

The raw CDP `AXValue#value`; type varies by control (e.g. a checkbox's value is a boolean).

#### properties : `Hash`

ARIA/computed properties flattened to `name => value`, e.g. `{"focusable" => true}`.

#### ignored? : `Boolean`

Whether the node is ignored by the accessibility tree.

#### ignored_reasons : `Array | nil`

Why the node is ignored, if it is.

#### node_id : `String | nil`
#### backend_dom_node_id : `Integer | nil`
#### child_ids : `Array | nil`

Ids linking the AX node back to its DOM node and children in the AX tree.

#### to_h : `Hash`

The raw CDP AXNode hash.

---

### 21 Workers (`docs/21-workers.md`)

---
sidebar_position: 21
---

# Workers

Ferrum discovers dedicated workers (`new Worker`), shared workers (`new SharedWorker`) and service workers
(`navigator.serviceWorker.register`) spawned anywhere in a context.

Dedicated and shared workers are connected to automatically as soon as they're discovered, there's no need to wait
for them. Service workers are treated differently: attaching to a service worker's session prevents Chrome from ever
terminating it while the connection stays open, so Ferrum only discovers them and leaves the connection alone unless
you explicitly ask for it with `attach_target`.

#### workers : `Array<Worker>`

Returns all dedicated and shared workers spawned by pages in this context, already connected.

```ruby
page.execute <<~JS
  new Worker(URL.createObjectURL(new Blob(
    ["self.onmessage = () => self.postMessage(1 + 1)"],
    { type: "application/javascript" }
  )))
JS

sleep 0.1 # give Ferrum a moment to discover and connect to it

worker = browser.workers.first # => #<Ferrum::Worker @target_id="..." @url="blob:...">
worker.evaluate("1 + 1") # => 2
```

#### service_workers : `Array<Target>`

Returns service worker targets registered in this context. Unlike `workers`, these are plain `Target` objects and
are not connected to -- call `attach_target` first if you want to interact with one.

```ruby
page.evaluate(%(await navigator.serviceWorker.register("/sw.js")))

sleep 0.1

target = browser.service_workers.first # => #<Ferrum::Target @id="..." @type="service_worker">
target.connected? # => false
```

#### attach_target(target_id) : `Boolean`

Manually attaches to a target discovered via `service_workers`. Once attached, `target.worker` returns a connected
`Worker` for it, same as for a dedicated or shared worker.

```ruby
target = browser.service_workers.first
browser.attach_target(target.id)
target.worker.evaluate("1 + 1") # => 2
```

Keep in mind this keeps the service worker alive for as long as the connection is open -- only attach to the ones
you actually intend to talk to.

## Worker

A dedicated or shared Worker spawned by a page. Unlike `Page` it has no DOM, frames, mouse/keyboard, or navigation
history -- just a single global execution context and its own network activity, reachable through `worker.network`.

#### target_id : `String`

The worker's CDP target id.

#### url : `String`

The worker's script URL, e.g. a `blob:` URL for workers created from an inline `Blob`, as is common for
dedicated/shared workers.

#### network : `Network`

Same API as `page.network` -- see [Network](/docs/ferrum/network). Useful for asserting on requests a worker made on
its own, independently of the page that spawned it.

```ruby
worker.network.wait_for_idle
worker.network.traffic # => [#<Ferrum::Network::Exchange, ...]
```

#### console : `Console`

Same API as `page.console` -- see [JavaScript](/docs/ferrum/javascript#console). Collects the worker's own
`console.*` calls and uncaught exceptions.

```ruby
worker.console.map(&:text) # => ["from worker"]
```

#### evaluate / execute / evaluate_handle

Same API as on `Page`/`Frame` -- see [JavaScript](/docs/ferrum/javascript). A worker has a single execution context,
so there's no frame to target.

```ruby
worker.evaluate("1 + 1") # => 2
```

#### on(:request) / on(:auth) / on(:console)

Subscribe to the worker's own network interception events, same as `page.on(:request)` -- see the `intercept` method
in [Network](/docs/ferrum/network), or to its new console messages.

#### close : `Boolean`

Closes the underlying CDP target and the worker's own connection.

```ruby
worker.close
```

## Example

Spawn a page's worker, wait for Ferrum to discover it, and read back a result it computes and posts to the page:

```ruby
page.go_to
page.execute <<~JS
  window.worker = new Worker(URL.createObjectURL(new Blob(
    ["self.onmessage = (e) => self.postMessage(e.data * 2)"],
    { type: "application/javascript" }
  )))
  window.worker.onmessage = (e) => { window.result = e.data }
JS

worker = nil
until worker
  worker = browser.workers.first
  sleep 0.05
end

page.execute("window.worker.postMessage(21)")
sleep 0.1
page.evaluate("window.result") # => 42
```

---

### 22 Docker (`docs/22-docker.md`)

---
sidebar_position: 22
---

# Docker

There are two ways to use Chrome with Docker:

- **Our images**, `ghcr.io/rubycdp/chrome` and `ghcr.io/rubycdp/chromium`: Chrome runs in its own container and Ferrum
  connects to it. Use these unless you need Chrome inside your app's container. Process reaping, crashpad, fonts and
  flags are taken care of.
- **Chrome in your own container**, next to your Ruby process. Chrome runs a bit differently in a container, in ways
  that are not obvious and that surface as slow leaks rather than errors, and [Running Chrome yourself](#running-chrome-yourself)
  covers all of it.

## Our images (recommended)

Two images, both for amd64 and arm64: `chromium` ships Debian's Chromium, `chrome` ships Google Chrome. Each
connection gets its own Chrome with a fresh profile, which is stopped as soon as the connection closes. Nothing else
talks to your browser: the driver inside passes CDP through unchanged.

Tags are `latest`, the major version (`155`) and the full version (`155.0.8059.84`). New browser releases are picked up
daily. Older tags stay available, so you can pin a version.

### Quick start

```shell
docker run --rm -p 3000:3000 ghcr.io/rubycdp/chromium
```

```ruby
browser = Ferrum::Browser.new(url: "http://localhost:3000")
browser.go_to("https://example.com")
browser.quit
```

`ws_url: "ws://localhost:3000"` works too.

### Compose and Cuprite

```yaml
services:
  chrome:
    image: ghcr.io/rubycdp/chromium
    environment:
      CONCURRENT: 4
    volumes:
      - .:/app

  app:
    build: .
    volumes:
      - .:/app
    environment:
      CHROME_URL: http://chrome:3000
    depends_on:
      chrome:
        condition: service_healthy
```

```ruby
Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(app, url: ENV.fetch("CHROME_URL"))
end

Capybara.server_host = "0.0.0.0"
Capybara.server_port = 3001
Capybara.app_host = "http://app:3001"
```

Chrome has to reach your app, so bind the server to `0.0.0.0` and point `app_host` at the app's name on the compose
network. When the app runs on your machine instead, use `host.docker.internal`, which needs
`extra_hosts: ["host.docker.internal:host-gateway"]` on Linux.

Cuprite keeps one connection per test process and calls `reset` between tests, so every parallel worker gets its own
Chrome. The image has a healthcheck on `/health`.

### Options per connection

Ferrum's `window_size:` and `browser_options:` only apply to a Chrome that Ferrum starts itself. For a remote one, put
them in the connection URL as `launch`, JSON or base64:

```ruby
launch = {
  defaultViewport: { width: 1920, height: 1080 },
  args: ["--accept-lang=de-DE"]
}
Ferrum::Browser.new(url: "http://localhost:3000?launch=#{CGI.escape(launch.to_json)}")
```

- `args`: extra Chrome flags. A flag with the same name as a default replaces it.
- `defaultViewport`: the window size, 1024x768 by default like Ferrum's.
- `ignoreDefaultArgs`: `true`, or a list of default flags to drop.
- `headless: false`: Chrome gets a window on its own virtual display.

`timeout=<ms>` in the same URL ends the session after that long. `docker run --rm ghcr.io/rubycdp/chromium --help`
lists everything, including the default flags, which are Ferrum's.

### Settings

- `CONCURRENT`: how many sessions run at once, the number of CPUs by default. Set it to at least your number of test
  workers.
- `QUEUED`: how many connections wait for a free slot, 10 by default. Past that they get `429`.
- `TIMEOUT`: a session timeout in milliseconds for every connection. There is none by default.
- `EXTERNAL`: the public URL when the container sits behind a proxy, so the WebSocket address it hands out is
  reachable.
- `PORT` and `HOST`: where it listens, `0.0.0.0:3000` by default.

### Files

Chrome resolves file paths inside its own container. Mount the directories that uploads come from and downloads go to
at the same absolute path in both containers, and `attach_file`, `select_file` and `save_path` work unchanged. Run the
container as your user so downloaded files belong to you:

```shell
docker run --rm -p 3000:3000 --user "$(id -u):$(id -g)" -v "$PWD:$PWD" ghcr.io/rubycdp/chromium
```

Screenshots and PDFs need no mount, they come back over CDP.

### Watching a test

Open [http://localhost:3000](http://localhost:3000) while tests run. The dashboard lists running sessions and their
pages, and each page links to DevTools with a live view of it. Pages go away when the test ends, so pause the test
(`binding.irb`, a breakpoint) to look at one.

### Security

:::warning
There is no authentication, and anyone who can connect can pass Chrome flags that run commands in the container. Never
publish the port beyond localhost or a private network.
:::

### Coming from browserless

Usually you swap the image and connect to `/`. The differences that matter:

- There's no session timeout by default, browserless kills sessions after 30 seconds.
- There are no `/chrome` or `/chromium` paths, no tokens, and no REST APIs (`/screenshot`, `/pdf`, `/function`).
  Ferrum does all of that over CDP.
- There's no `/workspace`, files go through mounts as described above.
- Nothing resets your viewport or download folder, and `reset` really isolates tests.

The full comparison and the reasons behind it are in the [design notes](https://github.com/rubycdp/ferrum/blob/main/docker/DESIGN.md).

## Running Chrome yourself

### In your app's container

:::note
When running in docker as root
:::

```ruby
Ferrum::Browser.new(dockerize: true)
```

Essentially it just sets CLI flags for a browser to make it start. On CI, you can just set `FERRUM_CHROME_DOCKERIZE=true`
environment variable, and it will be passed to all browser instances.

### An init is required

:::warning
If your Ruby process is pid 1, run the container with an init: `docker run --init`, `init: true` in Compose, or
`ENTRYPOINT ["/sbin/tini", "--"]` in the image. This is a requirement, not a nicety.
:::

Chrome is a process tree — a browser process plus renderer, GPU, zygote and utility processes — and it delegates the
last step of its own cleanup to the operating system.

When Chrome shuts down it tells its children to go and then exits about 10ms later. Its children take another
20-30ms to finish, whilst their parent is gone, so there's no one to collect them. This is not avoidable
and not specific to Ferrum: it happens whether the browser is asked to quit politely or killed outright, and whether
the signal goes to the browser alone or to its whole process group. Measured on Chrome 151, roughly eight to eleven
processes per browser finish after the browser itself has exited.

Unix answers this by handing orphans to pid 1, whose job is to collect them. On a normal machine that is systemd or
launchd and it happens instantly. In a container started as `CMD ["bundle", "exec", "..."]` there is no init at
all: *your* process is pid 1, it never inherited that job, and it collects nothing.

Two things then go wrong, both measured on the same image with and without `--init`:

|  | no init | with an init |
|---|---|---|
| processes left after each `#quit` | 9 `<defunct>`, never reclaimed | 0 |
| time `#quit` takes | 2.02s | 0.06s |

The leak is the obvious cost. Each `<defunct>` entry holds a pid and a process-table slot forever, so eventually
`fork` starts failing, new browsers will not start, and page loads time out for no visible reason:

```console
$ docker exec app ps -A
  PID TTY          TIME CMD
   41 ?        00:00:00 [chrome] <defunct>
   42 ?        00:00:00 [chrome] <defunct>
   62 ?        00:00:01 [chrome] <defunct>
   ...
```

The two seconds are less obvious. Ferrum waits for Chrome's process group to empty before returning, and a zombie
still answers `kill(0, ...)` — so with nothing reaping them the group never looks empty and every `#quit` waits out
its full kill timeout.

An init fixes both, and is worth having regardless: every other process your image spawns has the same problem.

### Kubernetes

There is no `init: true` in a pod spec, so you have two options.

**Bake an init into the image.** `ENTRYPOINT ["/sbin/tini", "--"]`. Scoped to your container, behaves the same
everywhere, and is the better default.

**Or set `shareProcessNamespace: true` on the pod.** Kubernetes then makes the `pause` container pid 1 for the whole
pod, and `pause` is an init — its `SIGCHLD` handler loops on `waitpid(-1, WNOHANG)`, which is exactly the job that
is otherwise missing. Note this is not mentioned on the Kubernetes documentation page for the feature; it is
visible in `pause.c`.

It has real trade-offs beyond reaping, though: every container in the pod can see the other processes, their
environment variables through `/proc/$pid/environ`, and their filesystems through `/proc/$pid/root`. Your process
also stops being pid 1, so anyone doing `kill -HUP 1` now signals `pause`.

:::note
`initContainers` are not an init system.
:::

### What Ferrum cleans up, and what it does not

Ferrum spawns exactly one process: the browser. On `#quit` it signals the browser's process group, escalating from
`TERM` to `KILL`, and waits on the browser itself — the one process it is the parent of, and therefore the only one
nobody else can collect.

Chrome's renderer, GPU and utility processes are Ferrum's *grand*children. When they outlive the browser the kernel
reparents them to pid 1, and collecting them is PID 1 job by definition. Ferrum deliberately does not do it:
`waitpid(-1)` would also collect child processes your own application spawned, stealing their exit statuses. A
library cannot take that liberty, which is why the answer here is an init rather than something Ferrum can fix.

`chrome_crashpad_handler` is a whole new story — Chrome double-forks it into its own session before any of this
happens, so it is not even in the process group Ferrum signals. See [the crashpad handler](/docs/ferrum/customization#the-crashpad-handler),
including why the flag that appears to disable it must not be used.

### Another remote browser service

Any service that speaks CDP works the same way, with `url:` or `ws_url:`. Ferrum did not spawn that browser and
couldn't kill it:

```ruby
browser = Ferrum::Browser.new(ws_url: "ws://chrome:3000/")
```

`#quit` closes the connection and nothing more. Whether the remote browser then shuts down is up to that service —
most reclaim a session when its connection drops, and most have a timeout for when they do not. To ask the browser
itself to exit rather than relying on that, use `#close`, which sends the CDP `Browser.close` command:

```ruby
browser.close
browser.quit
```

### Diagnosing

Check whether the processes piling up are dead or alive:

```console
$ docker exec app ps -Ao pid=,ppid=,stat=,comm=
```

`STAT` of `Z`, or `<defunct>` in the command, means the process is finished and merely uncollected. That is the
missing-init problem above.

Live processes accumulating instead means browsers are never being shut down at all — usually a `#quit` that is not
called, or a remote browser service holding sessions open. An init will not help with those.

---

### 3 Navigation (`docs/3-navigation.md`)

---
sidebar_position: 4
---

# Navigation

#### go_to(url) : `String`

Navigate page to.

* url `String` The url should include scheme unless you set `base_url` when
  configuring driver.

```ruby
page.go_to("https://github.com/")
```

#### back

Navigate to the previous page in history.

```ruby
page.go_to("https://github.com/")
page.at_xpath("//a").click
page.back
```

#### forward

Navigate to the next page in history.

```ruby
page.go_to("https://github.com/")
page.at_xpath("//a").click
page.back
page.forward
```

#### refresh

Reload current page.

```ruby
page.go_to("https://github.com/")
page.refresh
```

#### stop

Stop all navigations and loading pending resources on the page

```ruby
page.go_to("https://github.com/")
page.stop
```

#### wait_for_navigation(wait = 0.1) { } : `Boolean`

Runs the block and, if it starts a navigation within `wait` seconds, waits for the new page to load. Returns `false`
if it doesn't load within the page timeout. `click` already does this, use it for navigations started from
JavaScript.

* wait `Numeric` seconds to watch for a navigation after the block, `FERRUM_CLICK_WAIT` or `0.1` by default

```ruby
page.wait_for_navigation { page.at_css("a").evaluate("this.click()") } # => true
page.current_url # => the link's page
```

#### wait_for_navigation!(wait = 0.1) { }

Same as `wait_for_navigation`, but raises `Ferrum::TimeoutError` if the new page doesn't load within the page timeout.

```ruby
page.wait_for_navigation! { page.at_css("a").evaluate("this.click()") }
```

#### position = \*\*options

Set the position for the browser window

* options `Hash`
  * :left `Integer`
  * :top `Integer`

```ruby
browser.position = { left: 10, top: 20 }
```

#### position : `Array<Integer>`

Get the position for the browser window

```ruby
browser.position # => [10, 20]
```

#### window_bounds = \*\*options

Set window bounds

* options `Hash`
  * :left `Integer`
  * :top `Integer`
  * :width `Integer`
  * :height `Integer`
  * :window_state `String`

```ruby
browser.window_bounds = { left: 10, top: 20, width: 1024, height: 768, window_state: "normal" }
```

#### window_bounds : `Hash<String, Integer | String>`

Get window bounds

```ruby
browser.window_bounds # => { "left": 0, "top": 1286, "width": 10, "height": 10, "windowState": "normal" }
```

#### window_id : `Integer`

Current window id

```ruby
browser.window_id # => 1
```

---

### 4 Finders (`docs/4-finders.md`)

---
sidebar_position: 4
---

# Finders

#### at_css(selector, \*\*options) : `Node` | `nil`

Find node by selector. Runs `document.querySelector` within the document or
provided node.

* selector `String`
* options `Hash`
  * :within `Node` | `nil`

```ruby
page.go_to("https://github.com/")
page.at_css("a[aria-label='Issues you created']") # => Node
```

#### css(selector, \*\*options) : `Array<Node>` | `[]`

Find nodes by selector. The method runs `document.querySelectorAll` within the
document or provided node.

* selector `String`
* options `Hash`
  * :within `Node` | `nil`

```ruby
page.go_to("https://github.com/")
page.css("a[aria-label='Issues you created']") # => [Node]
```

#### at_xpath(selector, \*\*options) : `Node` | `nil`

Find node by xpath.

* selector `String`
* options `Hash`
  * :within `Node` | `nil`

```ruby
page.go_to("https://github.com/")
page.at_xpath("//a[@aria-label='Issues you created']") # => Node
```

#### xpath(selector, \*\*options) : `Array<Node>` | `[]`

Find nodes by xpath.

* selector `String`
* options `Hash`
  * :within `Node` | `nil`

```ruby
page.go_to("https://github.com/")
page.xpath("//a[@aria-label='Issues you created']") # => [Node]
```

#### current_url : `String`

Returns current top window location href.

```ruby
page.go_to("https://google.com/")
page.current_url # => "https://www.google.com/"
```

#### current_title : `String`

Returns current top window title

```ruby
page.go_to("https://google.com/")
page.current_title # => "Google"
```

#### body : `String`

Returns current page's html.

```ruby
page.go_to("https://google.com/")
page.body # => '<html itemscope="" itemtype="http://schema.org/WebPage" lang="ru"><head>...
```

---

### 5 Screenshots (`docs/5-screenshots.md`)

---
sidebar_position: 5
---

# Screenshots

#### screenshot(\*\*options) : `String` | `Integer`

Saves screenshot on a disk or returns it as base64.

* options `Hash`
  * :path `String` to save a screenshot on the disk. `:encoding` will be set to
    `:binary` automatically
  * :encoding `Symbol` `:base64` | `:binary` you can set it to return image as
    Base64
  * :format `String` "jpeg" ("jpg") | "png" | "webp"
  * :quality `Integer` 0-100 works for jpeg only
  * :full `Boolean` whether you need full page screenshot or a viewport
  * :selector `String` css selector for given element, optional
  * :area `Hash` area for screenshot, optional
    * :x `Integer`
    * :y `Integer`
    * :width `Integer`
    * :height `Integer`
  * :scale `Float` zoom in/out
  * :background_color `Ferrum::RGBA.new(0, 0, 0, 0.0)` to have specific background color

```ruby
page.go_to("https://google.com/")
# Save on the disk in PNG
page.screenshot(path: "google.png") # => 134660
# Save on the disk in JPG
page.screenshot(path: "google.jpg") # => 30902
# Save to Base64 the whole page not only viewport and reduce quality
page.screenshot(full: true, quality: 60, encoding: :base64) # "iVBORw0KGgoAAAANSUhEUgAABAAAAAMACAYAAAC6uhUNAAAAAXNSR0IArs4c6Q...
# Save on the disk with the selected element in PNG
page.screenshot(path: "google.png", selector: "textarea") # => 11340
# Save to Base64 with an area of the page in PNG
page.screenshot(path: "google.png", area: { x: 0, y: 0, width: 400, height: 300 }) # => 54239
# Save with specific background color
page.screenshot(background_color: Ferrum::RGBA.new(0, 0, 0, 0.0))
```

#### pdf(\*\*options) : `String` | `Boolean`

Saves PDF on a disk or returns it as base64.

* options `Hash`
  * :path `String` to save a pdf on the disk. `:encoding` will be set to
    `:binary` automatically
  * :encoding `Symbol` `:base64` | `:binary` you can set it to return pdf as
    Base64
  * :landscape `Boolean` paper orientation. Defaults to false.
  * :scale `Float` zoom in/out
  * :format `symbol` standard paper sizes :letter, :legal, :tabloid, :ledger, :A0, :A1, :A2, :A3, :A4, :A5, :A6

  * :paper_width `Float` set paper width
  * :paper_height `Float` set paper height
  * See other [native options](https://chromedevtools.github.io/devtools-protocol/tot/Page#method-printToPDF) you can pass

```ruby
page.go_to("https://google.com/")
# Save to disk as a PDF
page.pdf(path: "google.pdf", paper_width: 1.0, paper_height: 1.0) # => true
```

#### mhtml(\*\*options) : `String` | `Integer`

Saves MHTML on a disk or returns it as a string.

* options `Hash`
  * :path `String` to save a file on the disk.

```ruby
page.go_to("https://google.com/")
page.mhtml(path: "google.mhtml") # => 87742
```

---

### 6 Screencast (`docs/6-screencast.md`)

---
sidebar_position: 6
---

# Screencast

#### start_screencast(\*\*options) \{ |data, metadata, session_id| ... \}

Starts sending frames to record screencast to the given block.

* options `Hash`
  * :format `Symbol` `:jpeg` | `:png` The format the image should be returned in.
  * :quality `Integer` The image quality. **Note:** 0-100 works for JPEG only.
  * :max_width `Integer` Maximum screencast frame width.
  * :max_height `Integer` Maximum screencast frame height.
  * :every_nth_frame `Integer` Send every n-th frame.

* Block inputs:
  * data `String` Base64-encoded compressed image.
  * metadata `Hash` Screencast frame metadata.
    * "offsetTop" `Integer` Top offset in DIP.
    * "pageScaleFactor" `Integer` Page scale factor.
    * "deviceWidth" `Integer` Device screen width in DIP.
    * "deviceHeight" `Integer` Device screen height in DIP.
    * "scrollOffsetX" `Integer` Position of horizontal scroll in CSS pixels.
    * "scrollOffsetY" `Integer` Position of vertical scroll in CSS pixels.
    * "timestamp" `Float` (optional) Frame swap timestamp in seconds since Unix epoch.
  * session_id `Integer` Frame number.

```ruby
require "base64"

page.go_to("https://apple.com/ipad")

page.start_screencast(format: :jpeg, quality: 75) do |data, metadata|
  timestamp = (metadata["timestamp"] * 1000).to_i
  File.binwrite("image_#{timestamp}.jpg", Base64.decode64(data))
end

sleep 10

page.stop_screencast
```

> ### 📝 NOTE
>
> Chrome only sends new frames while page content is changing. For example, if
> there is an animation or a video on the page, Chrome sends frames at the rate
> requested. On the other hand, if the page is nothing but a wall of static text,
> Chrome sends frames while the page renders. Once Chrome has finished rendering
> the page, it sends no more frames until something changes (e.g., navigating to
> another location).

#### stop_screencast

Stops sending frames.

---

### 7 Network (`docs/7-network.md`)

---
sidebar_position: 7
---

# Network

`page.network`

#### traffic `Array<Network::Exchange>`

Returns all information about network traffic as `Network::Exchange` instance
which in general is a wrapper around `request`, `response` and `error`.

```ruby
page.go_to("https://github.com/")
page.network.traffic # => [#<Ferrum::Network::Exchange, ...]
```

#### request : `Network::Request`

Page request of the main frame.

```ruby
page.go_to("https://github.com/")
page.network.request # => #<Ferrum::Network::Request...
```

#### response : `Network::Response`

Page response of the main frame.

```ruby
page.go_to("https://github.com/")
page.network.response # => #<Ferrum::Network::Response...
```

#### status : `Integer`

Contains the status code of the main page response (e.g., 200 for a
success). This is just a shortcut for `response.status`.

```ruby
page.go_to("https://github.com/")
page.network.status # => 200
```

#### wait_for_idle(\*\*options) : `Boolean`

Waits for network idle, returns `true` in case of success and `false` if there are still connections.

* options `Hash`
    * :connections `Integer` how many connections are allowed for network to be
      idling, `0` by default
    * :duration `Float` sleep for given amount of time and check again, `0.05` by
      default
    * :timeout `Float` during what time we try to check idle, `browser.timeout`
      by default

```ruby
page.go_to("https://example.com/")
page.at_xpath("//a[text() = 'No UI changes button']").click
page.network.wait_for_idle # => true
```

#### wait_for_idle!(\*\*options)

Waits for network idle or raises `Ferrum::TimeoutError` error. Accepts same arguments as `wait_for_idle`.

```ruby
page.go_to("https://example.com/")
page.at_xpath("//a[text() = 'No UI changes button']").click
page.network.wait_for_idle! # might raise an error
```

#### clear(type)

Clear page's cache or collected traffic.

* type `Symbol` it is either `:traffic` or `:cache`

```ruby
traffic = page.network.traffic # => []
page.go_to("https://github.com/")
traffic.size # => 51
page.network.clear(:traffic)
traffic.size # => 0
```

#### on(name, &block) : `Integer`

Subscribes to a CDP event, or to one of the following pseudo-events. Returns a
subscription id you can pass to `off` to unsubscribe.

* `:request` an intercepted request, see `intercept` below. Requires `network.intercept` to be set up first.
* `:response` a request's response once it's fully loaded, see below. Works without `intercept`.
* `:auth` a proxy/basic auth challenge, see `authorize` below. Requires `network.intercept` to be set up first.
* `:dialog` a JS dialog (`alert`/`confirm`/`prompt`), see [Dialogs](./16-dialogs.md).
* anything else is passed straight through as a raw CDP event name, e.g. `page.on("Page.frameNavigated") { |params| ... }`.

##### on(:response, &block)

Subscribes to a callback fired once a request's response has fully loaded, i.e. after
Chrome's `Network.loadingFinished` event, so `exchange.response.body` is always
available. Doesn't require `intercept` to be set up. The block receives the
`Network::Exchange` for that request; it's never yielded for requests that fail to
load (use `traffic` to inspect those instead).

```ruby
page.on(:response) do |exchange|
  puts "#{exchange.response.status} #{exchange.url}"
end
page.go_to("https://github.com/")
```

#### off(name, id) : `void`

Unsubscribes a listener previously registered via `on`, given the subscription id `on` returned.

* name `Symbol | String` the same event name that was passed to `on`, e.g. `:request`, `:response`, `:auth`
* id `Integer` the subscription id returned by `on`

```ruby
id = page.on(:response) do |exchange|
  puts "#{exchange.response.status} #{exchange.url}"
end
page.go_to("https://github.com/")
page.off(:response, id)
```

#### intercept(\*\*options)

Set request interception for given options. This method is only sets request
interception, you should use `on` callback to catch requests and abort or
continue them.

Every paused request has to be continued, aborted or responded to by one of your `on(:request)` callbacks, including
ones that only log or inspect it. A request nobody handles stays paused, and the page waits for it until it times out.

* options `Hash`
    * :pattern `String` \* by default
    * :resource_type `Symbol` one of the [resource types](https://chromedevtools.github.io/devtools-protocol/tot/Network#type-ResourceType)

```ruby
browser = Ferrum::Browser.new
page = browser.create_page
page.network.intercept
page.on(:request) do |request|
  if request.match?(/bla-bla/)
    request.abort
  elsif request.match?(/lorem/)
    request.respond(body: "Lorem ipsum")
  else
    request.continue
  end
end
page.go_to("https://google.com")
```

#### authorize(\*\*options, &block)

If site or proxy uses authorization you can provide credentials using this method.

* options `Hash`
    * :type `Symbol` `:server` | `:proxy` site or proxy authorization
    * :user `String`
    * :password `String`
* &block accepts authenticated request, which you must subsequently allow or deny, if you don't
  care about unwanted requests just call `request.continue`.

```ruby
page.network.authorize(user: "login", password: "pass") { |req| req.continue }
page.go_to("http://example.com/authenticated")
puts page.network.status # => 200
puts page.body # => Welcome, authenticated client
```

Since Chrome implements authorize using request interception you must continue or abort authorized requests. If you
already have code that uses interception you can use `authorize` without block, but if not you are obliged to pass
block, so this is version doesn't pass block and can work just fine:

```ruby
browser = Ferrum::Browser.new
page = browser.create_page
page.network.intercept
page.on(:request) do |request|
  if request.resource_type == "Image"
    request.abort
  else
    request.continue
  end
end

page.network.authorize(user: "login", password: "pass", type: :proxy)

page.go_to("https://google.com")
```

You used to call `authorize` method without block, but since it's implemented using request interception there could be
a collision with another part of your code that also uses request interception, so that authorize allows the request
while your code denies but it's too late. The block is mandatory now.

#### emulate_network_conditions(\*\*options)

Activates emulation of network conditions.

* options `Hash`
    * :offline `Boolean` emulate internet disconnection, `false` by default
    * :latency `Integer` minimum latency from request sent to response headers received (ms), `0` by
      default
    * :download_throughput `Integer` maximal aggregated download throughput (bytes/sec), `-1`
      by default, disables download throttling
    * :upload_throughput `Integer` maximal aggregated upload throughput (bytes/sec), `-1`
      by default, disables download throttling
    * :connection_type `String` connection type if known, one of: none, cellular2g, cellular3g, cellular4g,
      bluetooth, ethernet, wifi, wimax, other. `nil` by default

```ruby
page.network.emulate_network_conditions(connection_type: "cellular2g")
page.go_to("https://github.com/")
```

#### offline_mode

Activates offline mode for a page.

```ruby
page.network.offline_mode
page.go_to("https://github.com/") # => Ferrum::StatusError (Request to https://github.com/ failed(net::ERR_INTERNET_DISCONNECTED))
```

#### cache(disable: `Boolean`)

Toggles ignoring cache for each request. If true, cache will not be used.

```ruby
page.network.cache(disable: true)
```

---

### 8 Downloads (`docs/8-downloads.md`)

---
sidebar_position: 8
---

# Downloads

`page.downloads`

#### files `Array<Hash>`

Returns all information about downloaded files as a `Hash`.

```ruby
page.go_to("http://localhost/attachment.pdf")
page.downloads.files # => [{"frameId"=>"E3316DF1B5383D38F8ADF7485005FDE3", "guid"=>"11a68745-98ac-4d54-9b57-9f9016c268b3", "url"=>"http://localhost/attachment.pdf", "suggestedFilename"=>"attachment.pdf", "totalBytes"=>4911, "receivedBytes"=>4911, "state"=>"completed"}]
```

#### wait(timeout)

Waits until the download is finished.

```ruby
page.go_to("http://localhost/attachment.pdf")
page.downloads.wait
```

or

```ruby
page.go_to("http://localhost/page")
page.downloads.wait { page.at_css("#download").click }
```

#### set_behavior(\*\*options)

Sets behavior in case of file to be downloaded.

* options `Hash`
    * :save_path `String` absolute path of where to store the file
    * :behavior `Symbol` `deny | allow | allowAndName | default`, `allow` by default

```ruby
page.go_to("https://example.com/")
page.downloads.set_behavior(save_path: "/tmp", behavior: :allow)
```

---

### 9 Proxy (`docs/9-proxy.md`)

---
sidebar_position: 9
---

# Proxy

You can set a proxy with a `:proxy` option:

```ruby
Ferrum::Browser.new(proxy: { host: "x.x.x.x", port: "8800", user: "user", password: "pa$$" })
```

`:bypass` can specify semi-colon-separated list of hosts for which proxy shouldn't be used:

```ruby
Ferrum::Browser.new(proxy: { host: "x.x.x.x", port: "8800", bypass: "*.google.com;*foo.com" })
```

In general passing a proxy option when instantiating a browser results in a browser running with proxy command line
flags, so that it affects all pages and contexts. You can create a page in a new context which can use its own proxy
settings:

```ruby
browser = Ferrum::Browser.new

browser.create_page(proxy: { host: "x.x.x.x", port: 31337, user: "user", password: "password" }) do |page|
  page.go_to("https://api.ipify.org?format=json")
  page.body # => "x.x.x.x"
end

browser.create_page(proxy: { host: "y.y.y.y", port: 31337, user: "user", password: "password" }) do |page|
  page.go_to("https://api.ipify.org?format=json")
  page.body # => "y.y.y.y"
end
```

## Intercepting requests behind a proxy

A proxy with `:user` and `:password` is authorized through request interception, so Ferrum continues every request
for you as long as nothing else subscribes to `on(:request)`. Once you subscribe, or set `network.blacklist` or
`network.whitelist`, your code takes over and has to continue or abort every request:

```ruby
browser = Ferrum::Browser.new(proxy: { host: "x.x.x.x", port: "8800", user: "user", password: "pa$$" })
browser.network.intercept
browser.on(:request) do |request|
  if request.resource_type == "Image"
    request.abort
  else
    request.continue
  end
end
browser.go_to("https://github.com/")
```

## Built-in proxy server

Ferrum also ships a small forwarding proxy server, `Ferrum::Proxy`, which is handy
for tests or when you need to rotate upstream proxies at runtime:

```ruby
require "ferrum/proxy"

proxy = Ferrum::Proxy.start(host: "127.0.0.1", port: 0)
browser = Ferrum::Browser.new(proxy: { host: proxy.host, port: proxy.port })
```

`Ferrum::Proxy` is built on top of [`webrick`](https://rubygems.org/gems/webrick),
which is **not** a dependency of Ferrum. If you use it, add webrick to your Gemfile:

```ruby
gem "webrick"
```

---

--- METRICS ---
- Files Extracted: 23
- Estimated Token Budget: ~21632 tokens
- Canonical Reference: https://codewiki.google/github.com/rubycdp/ferrum
