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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
- `browser.add_extension` adds an extension for pages and iframes attached afterwards [#663]
- `page.isolated_world(name)` and `frame.isolated_world(name)` evaluate JavaScript in a world hidden from the page
- `Node#backend_node_id` and `Node#node_type`
- `page.wait_for_navigation { }` and `wait_for_navigation! { }` wait for a navigation the block starts, e.g. a click
made from JavaScript [#679]

### Changed
- `evaluate` fetches the whole result in one CDP call instead of one per nested object, dates come back as ISO 8601
Expand Down
21 changes: 21 additions & 0 deletions docs/3-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ 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
Expand Down
1 change: 1 addition & 0 deletions lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Browser
delegate %i[default_context] => :contexts
delegate %i[targets create_target page pages windows workers service_workers attach_target] => :default_context
delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
wait_for_navigation wait_for_navigation!
at_css at_xpath css xpath wait_for_selector current_url current_title url title
body doctype content=
headers cookies network console accessibility downloads
Expand Down
53 changes: 40 additions & 13 deletions lib/ferrum/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,40 @@ def wait_for_reload(timeout = 1)
@event.set
end

#
# Runs the block and, if it starts a navigation of the main frame within
# `wait` seconds, waits for that navigation to finish, e.g. after a click
# on a link made from JavaScript.
#
# @param [Numeric] wait
# How many seconds to watch for a navigation after the block returns.
#
# @yield The code that may start a navigation.
#
# @return [Boolean]
# `false` when a navigation started but didn't finish within the page's
# `timeout`.
#
# @example
# page.wait_for_navigation { page.at_css("a").evaluate("this.click()") } # => true
#
def wait_for_navigation(wait = Mouse::CLICK_WAIT)
iteration = @event.reset
yield
@event.wait(wait)
iteration == @event.iteration || @event.wait(timeout)
end

#
# Waits for a navigation the block starts or raises {Ferrum::TimeoutError}.
# Accepts the same arguments as {#wait_for_navigation}.
#
# @raise [Ferrum::TimeoutError]
#
def wait_for_navigation!(...)
raise TimeoutError unless wait_for_navigation(...)
end

#
# Enables/disables CSP bypass.
#
Expand Down Expand Up @@ -496,21 +530,14 @@ def activate
# page.command("Page.navigate", url: "https://github.com/")
#
def command(method, wait: 0, slowmoable: false, timeout: nil, **params)
iteration = @event.reset if wait.positive?
sleep(@options.slowmo) if slowmoable && @options.slowmo.positive?
result = client.command(method, timeout: timeout || self.timeout, **params)

if wait.positive?
# Wait a bit after command and check if iteration has
# changed, which means there was some network event for
# the main frame, and it started to load new content.
@event.wait(wait)
if iteration != @event.iteration
set = @event.wait(self.timeout)
raise TimeoutError unless set
end
result = nil
wait_for_navigation!(wait) { result = command(method, slowmoable:, timeout:, **params) }
return result
end
result

sleep(@options.slowmo) if slowmoable && @options.slowmo.positive?
client.command(method, timeout: timeout || self.timeout, **params)
end

# Subscribes to a CDP event, or to `:dialog`, `:request`, `:response`,
Expand Down
4 changes: 4 additions & 0 deletions sig/ferrum/page.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ module Ferrum

def wait_for_reload: (?::Numeric timeout) -> void

def wait_for_navigation: (?::Numeric wait) { () -> void } -> bool

def wait_for_navigation!: (?::Numeric wait) { () -> void } -> void

def bypass_csp: (?enabled: bool) -> Hash[String, untyped]

def evaluate_on_new_document: (String source, ?world_name: String?, ?run_immediately: bool) -> String
Expand Down
48 changes: 48 additions & 0 deletions spec/page_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,54 @@
end
end

describe "#wait_for_navigation" do
before { page.go_to("/") }

it "waits for a navigation the block starts" do
navigated = loaded = false
page.on("Page.frameNavigated") { |params| navigated = params.dig("frame", "url").end_with?("/slow") }
page.on("Page.loadEventFired") { loaded = navigated }

expect(page.wait_for_navigation { page.execute("location.href = '/slow'") }).to be(true)
expect(loaded).to be(true)
expect(page.body).to include("slow page")
end

it "returns after the wait when nothing navigates" do
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)

expect(page.wait_for_navigation(0.2) { page.execute("1 + 1") }).to be(true)
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be_between(0.2, page.timeout)
end

it "returns false when the navigation doesn't finish within the page timeout" do
page.timeout = 1

expect(page.wait_for_navigation { page.execute("location.href = '/really_slow'") }).to be(false)
end
end

describe "#wait_for_navigation!" do
before { page.go_to("/") }

it "waits for a navigation the block starts" do
navigated = loaded = false
page.on("Page.frameNavigated") { |params| navigated = params.dig("frame", "url").end_with?("/slow") }
page.on("Page.loadEventFired") { loaded = navigated }

page.wait_for_navigation! { page.execute("location.href = '/slow'") }

expect(loaded).to be(true)
end

it "raises when the navigation doesn't finish within the page timeout" do
page.timeout = 1

expect { page.wait_for_navigation! { page.execute("location.href = '/really_slow'") } }
.to raise_error(Ferrum::TimeoutError)
end
end

describe "#bypass_csp" do
it "can bypass csp headers" do
page.go_to("/csp")
Expand Down
Loading