Skip to content

Commit cbdb75e

Browse files
committed
chore: update
1 parent 1173d6a commit cbdb75e

11 files changed

Lines changed: 453 additions & 7 deletions

File tree

docs/ferrum/1-introduction.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ browser.reset
118118
browser.quit
119119
```
120120

121+
`#quit` blocks by default until the browser process is confirmed dead and its user data directory removed. Killing a
122+
stubborn process (one that ignores `TERM` and needs `KILL`) can take a couple of seconds, which matters if you're
123+
quitting many browsers in a hot path. Pass `wait: false` to return immediately and run that cleanup on a background
124+
thread instead:
125+
126+
```ruby
127+
browser = Ferrum::Browser.new
128+
thread = browser.quit(wait: false)
129+
# ... do other work while the browser is killed and its directory removed in the background ...
130+
thread.join # only needed if you must wait for cleanup to finish, e.g. before reusing a fixed port
131+
```
132+
121133
## Thread safety
122134

123135
Ferrum is fully thread-safe. You can create one browser or a few as you wish and

docs/ferrum/14-emulation.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,26 @@ sidebar_position: 14
66

77
#### set_viewport
88

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

1113
* options `Hash`
1214
* :width `Integer`, viewport width. `0` by default
1315
* :height `Integer`, viewport height. `0` by default
1416
* :scale_factor `Float`, device scale factor. `0` by default
15-
* :mobile `Boolean`, whether to emulate mobile device. `false` by default
17+
* :mobile `Boolean`, whether to emulate mobile device and enable touch. `false` by default
1618

1719
```ruby
1820
page.set_viewport(width: 1000, height: 600, scale_factor: 3)
1921
```
22+
23+
`:width`, `:height`, and `:scale_factor` all default to `0`, which per the
24+
[DevTools protocol](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride)
25+
disables overriding that particular value. This means `mobile:` can be
26+
enabled on its own, at the page's current size, without specifying a device
27+
size preset:
28+
29+
```ruby
30+
page.set_viewport(mobile: true)
31+
```

docs/ferrum/15-frames.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,25 @@ One of the states frame's in:
7474
* `:navigated`
7575
* `:stopped_loading`
7676

77+
#### loader_id : `String | nil`
78+
79+
Id of the loader associated with the frame's current navigation, set once the frame navigates.
80+
81+
#### lifecycle_events : `Array[Hash]`
82+
83+
List of the frame's lifecycle events as reported by the browser, each a `{"name" => String, "timestamp" => Float}`
84+
hash, e.g. `init`, `networkIdle`, `firstPaint`.
85+
86+
#### idle? : `Boolean`
87+
88+
Whether the frame has finished loading (`:stopped_loading` state). Frames in `:canceled` state (execution context
89+
torn down mid-navigation) are not considered idle.
90+
91+
```ruby
92+
page.go_to("https://example.com")
93+
page.main_frame.idle? # => true
94+
```
95+
7796
#### url : `String`
7897

7998
Returns current frame's location href.

docs/ferrum/2-customization.md

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ Ferrum::Browser.new(options)
1515
* `:incognito` (Boolean) - Create an incognito profile for the browser startup window, `true` by default.
1616
* `:dockerize` (Boolean) - Provide CLI flags to the browser to run it in a container, `false` by default.
1717
* `:xvfb` (Boolean) - Run browser in a virtual framebuffer, `false` by default.
18-
* `:flatten` (Boolean) - Use one websocket connection to the browser and all the pages in flatten mode.
18+
* `:flatten` (Boolean) - Use one websocket connection to the browser and all the pages in flatten mode,
19+
`true` by default. When set to `false`, each page/target opens its own dedicated websocket connection instead
20+
of sharing the browser's connection.
1921
* `:window_size` (Array) - The dimensions of the browser window in which to
2022
test, expressed as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
2123
* `:extensions` (Array[String | Hash]) - An array of paths to files or JS
@@ -42,7 +44,9 @@ Ferrum::Browser.new(options)
4244
options you put in `:browser_options` will be passed to the browser,
4345
except required ones of course.
4446
* `:port` (Integer) - Remote debugging port for headless Chrome.
45-
* `:host` (String) - Remote debugging address for headless Chrome.
47+
* `:host` (String) - Host we communicate with when spawning browser, `127.0.0.1` by default.
48+
Chrome always listens on `127.0.0.1` regardless of this option, so the host must resolve to `127.0.0.1` for
49+
Ferrum to actually be able to connect.
4650
* `:url` (String) - URL for a running instance of Chrome. If this is set, a
4751
browser process will not be spawned.
4852
* `:ws_url` (String) - Websocket url for a running instance of Chrome. If this is set, a
@@ -56,6 +60,78 @@ Ferrum::Browser.new(options)
5660
* `:save_path` (String) - Path to save attachments with [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header.
5761
* `:env` (Hash) - Environment variables you'd like to pass through to the process
5862

63+
## The crashpad handler
64+
65+
Chrome starts two `chrome_crashpad_handler` processes per browser on Linux. Their only job is to collect and upload
66+
crash reports, which no automated browser has any use for, but **Ferrum does not disable them**, because there is no
67+
safe way to.
68+
69+
Handlers Chrome starts on Linux:
70+
71+
| Chrome | `--headless` | `--headless=new` | headful |
72+
|---|---|---|---|
73+
| 127 and older | 0 | 2 | 2 |
74+
| 128 and newer | 2 | 2 | 2 |
75+
76+
New headless always starts them. What changed in 128 is what bare `--headless` *means*: before it, that selected old
77+
headless, a separate lightweight shell with no crash handler; from 128 on it selects new headless, which is full
78+
Chrome. Headful has always behaved like new headless.
79+
80+
### No flag turns them off safely
81+
82+
| Flag | What it does |
83+
|---|---|
84+
| `--disable-breakpad` | Stops crash *reporting*, not the handler process |
85+
| `--disable-crash-reporter` | Same |
86+
| `--no-crashpad` | Not a Chromium switch at all, so Chrome ignores it |
87+
| `--disable-crashpad-for-testing` | Real, and it does stop them — but it breaks the browser |
88+
89+
:::danger
90+
Do not pass `--disable-crashpad-for-testing`. It is named `for-testing` because it is meant for Chromium's own test
91+
harness, where the process tree is fully controlled. In a normally launched browser, child processes die at startup
92+
with `Crashing due to FD ownership violation`, and the network service crash-loops.
93+
94+
The failure is near-invisible: the browser process survives, so it starts normally and CDP answers every command.
95+
There is simply no network service behind it, so every navigation returns `net::ERR_ABORTED` and the document stays
96+
`about:blank`.
97+
:::
98+
99+
Ferrum shipped `--no-crashpad` as a default from 0.18.0 in an attempt at this. It never did anything, and has been
100+
removed.
101+
102+
### Operating system differences
103+
104+
On macOS one handler starts rather than two, and it exits when the browser does, so nothing accumulates.
105+
106+
It is also invisible to `pstree`, because it double-forks and reparents to launchd and is therefore never a
107+
descendant of the process that started Chrome. Look for it globally instead:
108+
109+
```console
110+
$ ps -Ao pid=,ppid=,comm= | grep crashpad
111+
13270 1 .../Helpers/chrome_crashpad_handler
112+
```
113+
114+
`ppid` is `1`, and the pid usually lands just after Chrome's own.
115+
116+
### Why this matters most in Docker
117+
118+
The handler is not a problem outside a container, and killing the browser is not what deals with it. The handler is
119+
independent of Chrome's process group, so the signal Ferrum sends during teardown never reaches it — and does not
120+
need to. The handler watches the browser and exits by itself once Chrome is gone. Verified by sending `TERM` and
121+
`KILL` to the process group, and `KILL` and `SIGUSR1` to the browser pid directly: in every case it terminated on its
122+
own within a second.
123+
124+
What is left behind is an exit status. Because the handler double-forks away from Chrome, its parent is pid 1, and
125+
pid 1 is what reaps it — systemd or launchd, silently, so nothing is left over and no cleanup is needed.
126+
127+
In a container your own process is usually pid 1, and it does not wait on children it never spawned. Nothing reaps
128+
the handlers, so every browser leaves two more `<defunct>` entries behind, and they are never reclaimed.
129+
**They accumulate, until the process table fills up.**
130+
131+
Run the container with an init — `docker run --init`, `init: true` in Compose, or tini as the entrypoint. It is worth
132+
doing regardless, since every other process in the image has the same problem, and there is no flag that avoids the
133+
need for it.
134+
59135
## Examples
60136

61137
```ruby

docs/ferrum/20-accessibility.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
sidebar_position: 20
3+
---
4+
5+
# Accessibility
6+
7+
Ferrum wraps the CDP [Accessibility](https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/) domain,
8+
letting you read the accessibility (AX) tree that assistive technologies (like screen readers) rely on. The query
9+
methods below work without calling `enable` first; `enable`/`disable` exist only if you want live AX events.
10+
11+
`page.accessibility` returns the `Ferrum::Accessibility` instance for a page.
12+
13+
:::note
14+
The node-scoped methods (`node_for`, `partial_tree`, and `query` with a `node:`) issue their command against the
15+
node's owning page session. They support same-process (same-target) iframes; nodes living in an out-of-process
16+
iframe (OOPIF, a separate CDP target) are not resolvable and will error or return an empty result.
17+
:::
18+
19+
#### node_for(node) : `AXNode | nil`
20+
21+
Returns the single non-ignored `AXNode` for a DOM node, or `nil`.
22+
23+
* node `Ferrum::Node`
24+
25+
```ruby
26+
node = page.at_css("button")
27+
page.accessibility.node_for(node) # => #<Ferrum::Accessibility::AXNode ...>
28+
```
29+
30+
You can also get to it directly from the node itself:
31+
32+
#### axnode : `AXNode | nil`
33+
34+
`Node#axnode` is a shortcut for `page.accessibility.node_for(self)`.
35+
36+
```ruby
37+
page.at_css("button").axnode.role # => "button"
38+
```
39+
40+
#### partial_tree(\*\*options) : `Array[AXNode]`
41+
42+
Returns the partial AX tree for a DOM node.
43+
44+
* options `Hash`
45+
* :node `Ferrum::Node` **required**
46+
* :fetch_relatives `Boolean` whether to include related nodes, `false` by default
47+
48+
```ruby
49+
node = page.at_css("form")
50+
page.accessibility.partial_tree(node: node)
51+
```
52+
53+
#### snapshot(\*\*options) : `Array[AXNode]`
54+
55+
Returns the full AX tree for the page.
56+
57+
* options `Hash`
58+
* :depth `Integer` how many levels deep to fetch, unlimited by default
59+
* :frame_id `String` restrict the snapshot to a given frame
60+
61+
```ruby
62+
page.accessibility.snapshot
63+
```
64+
65+
#### root(\*\*options) : `AXNode | nil`
66+
67+
Returns the root `AXNode` of the (optionally framed) document.
68+
69+
* options `Hash`
70+
* :frame_id `String` restrict to a given frame
71+
72+
```ruby
73+
page.accessibility.root
74+
```
75+
76+
#### query(\*\*options) : `Array[AXNode]`
77+
78+
Query the AX tree by accessible name and/or role.
79+
80+
* options `Hash`
81+
* :name `String` accessible name to match
82+
* :role `String` AX role to match, e.g. `"button"`
83+
* :node `Ferrum::Node` scope the query to this node's subtree, whole page by default
84+
85+
```ruby
86+
page.accessibility.query(role: "button")
87+
page.accessibility.query(name: "Submit", role: "button")
88+
```
89+
90+
#### enable : `self`
91+
#### disable : `self`
92+
93+
Enable/disable the CDP Accessibility domain, only needed if you want live AX events; the query methods above work
94+
without it.
95+
96+
## AXNode
97+
98+
Represents a single [AXNode](https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/#type-AXNode)
99+
from the CDP Accessibility domain. Instances are returned by the methods above, never constructed directly.
100+
101+
#### role : `String | nil`
102+
103+
The AX role, e.g. `"button"`, `"heading"`.
104+
105+
#### name : `String | nil`
106+
107+
The accessible name.
108+
109+
#### description : `String | nil`
110+
111+
The accessible description.
112+
113+
#### value : `String | Numeric | Boolean | nil`
114+
115+
The raw CDP `AXValue#value`; type varies by control (e.g. a checkbox's value is a boolean).
116+
117+
#### properties : `Hash`
118+
119+
ARIA/computed properties flattened to `name => value`, e.g. `{"focusable" => true}`.
120+
121+
#### ignored? : `Boolean`
122+
123+
Whether the node is ignored by the accessibility tree.
124+
125+
#### ignored_reasons : `Array | nil`
126+
127+
Why the node is ignored, if it is.
128+
129+
#### node_id : `String | nil`
130+
#### backend_dom_node_id : `Integer | nil`
131+
#### child_ids : `Array | nil`
132+
133+
Ids linking the AX node back to its DOM node and children in the AX tree.
134+
135+
#### to_h : `Hash`
136+
137+
The raw CDP AXNode hash.

0 commit comments

Comments
 (0)