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
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,20 +221,20 @@ browser.xpath("//a[@aria-label='Issues you created']") # => [Node]

#### current_url : `String`

Returns current window location href.
Returns current top window location href.

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

#### title : `String`
#### current_title : `String`

Returns current window title
Returns current top window title

```ruby
browser.goto("https://google.com/")
browser.title # => "Google"
browser.current_title # => "Google"
```

#### body : `String`
Expand Down Expand Up @@ -604,19 +604,17 @@ browser.execute(%(1 + 1)) # => true

## Frames

#### frame_url
#### frame_title
#### within_frame(frame, &block)
#### frames
#### main_frame
#### frame_by

Play around inside given frame

```ruby
browser.goto("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
frame = browser.at_xpath("//iframe")
browser.within_frame(frame) do
puts browser.frame_title # => HTML Demo: <iframe>
puts browser.frame_url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
end
frame = browser.frames[1]
puts frame.title # => HTML Demo: <iframe>
puts frame.url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
```


Expand Down
2 changes: 1 addition & 1 deletion lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class Browser
headers cookies network
mouse keyboard
screenshot pdf viewport_size
frames frame_by main_frame
evaluate evaluate_on evaluate_async execute
frame_url frame_title within_frame
on] => :page

attr_reader :client, :process, :contexts, :logger, :js_errors,
Expand Down
47 changes: 47 additions & 0 deletions lib/ferrum/frame.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require "ferrum/frame/dom"
require "ferrum/frame/runtime"

module Ferrum
class Frame
include DOM, Runtime

attr_reader :id, :parent_id, :page, :state, :name
attr_writer :execution_id

def initialize(page, id, parent_id = nil)
@page, @id, @parent_id = page, id, parent_id
end

# Can be one of:
# * started_loading
# * navigated
# * scheduled_navigation
# * cleared_scheduled_navigation
# * stopped_loading
def state=(value)
@state = value
end

def url
evaluate("document.location.href")
end

def title
evaluate("document.title")
end

def main?
@parent_id.nil?
end

def execution_id
raise NoExecutionContextError unless @execution_id
@execution_id
rescue NoExecutionContextError
@page.event.reset
@page.event.wait(@page.timeout) ? retry : raise
end
end
end
23 changes: 12 additions & 11 deletions lib/ferrum/page/dom.rb → lib/ferrum/frame/dom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
# inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
# details (DOM.describeNode).
module Ferrum
class Page
class Frame
module DOM
def current_url
evaluate("window.top.location.href")
end

def title
def current_title
evaluate("window.top.document.title")
end

Expand Down Expand Up @@ -57,22 +57,23 @@ def xpath(selector, within: nil)
} else {
throw error;
}
}), timeout, selector, within)
}), @page.timeout, selector, within)
end

# FIXME css doesn't work for a frame w/o execution_id
def css(selector, within: nil)
node_id = within&.node_id || @document_id
node_id = within&.node_id || @page.document_id

ids = command("DOM.querySelectorAll",
nodeId: node_id,
selector: selector)["nodeIds"]
ids = @page.command("DOM.querySelectorAll",
nodeId: node_id,
selector: selector)["nodeIds"]
ids.map { |id| build_node(id) }.compact
end

def at_css(selector, within: nil)
node_id = within&.node_id || @document_id
node_id = within&.node_id || @page.document_id

id = command("DOM.querySelector",
id = @page.command("DOM.querySelector",
nodeId: node_id,
selector: selector)["nodeId"]
build_node(id)
Expand All @@ -81,8 +82,8 @@ def at_css(selector, within: nil)
private

def build_node(node_id)
description = command("DOM.describeNode", nodeId: node_id)
Node.new(self, target_id, node_id, description["node"])
description = @page.command("DOM.describeNode", nodeId: node_id)
Node.new(self, @page.target_id, node_id, description["node"])
end
end
end
Expand Down
22 changes: 11 additions & 11 deletions lib/ferrum/page/runtime.rb → lib/ferrum/frame/runtime.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Ferrum
class Page
class Frame
module Runtime
INTERMITTENT_ATTEMPTS = ENV.fetch("FERRUM_INTERMITTENT_ATTEMPTS", 6).to_i
INTERMITTENT_SLEEP = ENV.fetch("FERRUM_INTERMITTENT_SLEEP", 0.1).to_f
Expand Down Expand Up @@ -49,13 +49,13 @@ def evaluate_on(node:, expression:, by_value: true, wait: 0)
attempts, sleep = INTERMITTENT_ATTEMPTS, INTERMITTENT_SLEEP

Ferrum.with_attempts(errors: errors, max: attempts, wait: sleep) do
response = command("DOM.resolveNode", nodeId: node.node_id)
response = @page.command("DOM.resolveNode", nodeId: node.node_id)
object_id = response.dig("object", "objectId")
options = DEFAULT_OPTIONS.merge(objectId: object_id)
options[:functionDeclaration] = options[:functionDeclaration] % expression
options.merge!(returnByValue: by_value)

response = command("Runtime.callFunctionOn",
response = @page.command("Runtime.callFunctionOn",
wait: wait, **options)["result"]
.tap { |r| handle_error(r) }

Expand All @@ -76,10 +76,10 @@ def call(*args, expression:, wait_time: nil, handle: true, **options)
params[:functionDeclaration] = params[:functionDeclaration] % expression
params = params.merge(arguments: arguments)
unless params[:executionContextId]
params = params.merge(executionContextId: execution_context_id)
params = params.merge(executionContextId: execution_id)
end

response = command("Runtime.callFunctionOn",
response = @page.command("Runtime.callFunctionOn",
**params)["result"].tap { |r| handle_error(r) }

handle ? handle_response(response) : response
Expand Down Expand Up @@ -115,9 +115,9 @@ def handle_response(response)
# and node destroyed so we need to retrieve it each time for given id.
# Though we can try to subscribe to `DOM.childNodeRemoved` and
# `DOM.childNodeInserted` in the future.
node_id = command("DOM.requestNode", objectId: object_id)["nodeId"]
description = command("DOM.describeNode", nodeId: node_id)["node"]
Node.new(self, target_id, node_id, description)
node_id = @page.command("DOM.requestNode", objectId: object_id)["nodeId"]
description = @page.command("DOM.describeNode", nodeId: node_id)["node"]
Node.new(self, @page.target_id, node_id, description)
when "array"
reduce_props(object_id, []) do |memo, key, value|
next(memo) unless (Integer(key) rescue nil)
Expand All @@ -140,7 +140,7 @@ def handle_response(response)
def prepare_args(args)
args.map do |arg|
if arg.is_a?(Node)
resolved = command("DOM.resolveNode", nodeId: arg.node_id)
resolved = @page.command("DOM.resolveNode", nodeId: arg.node_id)
{ objectId: resolved["object"]["objectId"] }
elsif arg.is_a?(Hash) && arg["objectId"]
{ objectId: arg["objectId"] }
Expand All @@ -154,7 +154,7 @@ def reduce_props(object_id, to)
if cyclic?(object_id).dig("result", "value")
return "(cyclic structure)"
else
props = command("Runtime.getProperties", objectId: object_id)
props = @page.command("Runtime.getProperties", objectId: object_id)
props["result"].reduce(to) do |memo, prop|
next(memo) unless prop["enumerable"]
yield(memo, prop["name"], prop["value"])
Expand All @@ -163,7 +163,7 @@ def reduce_props(object_id, to)
end

def cyclic?(object_id)
command("Runtime.callFunctionOn",
@page.command("Runtime.callFunctionOn",
objectId: object_id,
returnByValue: true,
functionDeclaration: <<~JS
Expand Down
2 changes: 1 addition & 1 deletion lib/ferrum/network.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def subscribe
end

exchange = Network::Exchange.new(params)
@exchange = exchange if exchange.navigation_request?(@page.frame_id)
@exchange = exchange if exchange.navigation_request?(@page.main_frame.id)
@traffic << exchange
end

Expand Down
13 changes: 11 additions & 2 deletions lib/ferrum/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ module Ferrum
class Node
attr_reader :page, :target_id, :node_id, :description, :tag_name

def initialize(page, target_id, node_id, description)
@page, @target_id = page, target_id
def initialize(frame, target_id, node_id, description)
@page = frame.page
@target_id = target_id
@node_id, @description = node_id, description
@tag_name = description["nodeName"].downcase
end
Expand All @@ -14,6 +15,14 @@ def node?
description["nodeType"] == 1 # nodeType: 3, nodeName: "#text" e.g.
end

def frame_id
description["frameId"]
end

def frame
page.frame_by(id: frame_id)
end

def focus
tap { page.command("DOM.focus", nodeId: node_id) }
end
Expand Down
32 changes: 16 additions & 16 deletions lib/ferrum/page.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# frozen_string_literal: true

require "forwardable"
require "ferrum/mouse"
require "ferrum/keyboard"
require "ferrum/headers"
require "ferrum/cookies"
require "ferrum/dialog"
require "ferrum/network"
require "ferrum/page/dom"
require "ferrum/page/runtime"
require "ferrum/page/frame"
require "ferrum/page/frames"
require "ferrum/page/screenshot"
require "ferrum/browser/client"

Expand All @@ -28,21 +27,23 @@ def reset
end
end

include DOM, Runtime, Frame, Screenshot
extend Forwardable
delegate %i[at_css at_xpath css xpath
current_url current_title url title body
execution_id evaluate evaluate_on evaluate_async execute] => :main_frame

include Frames, Screenshot

attr_accessor :referrer
attr_reader :target_id, :browser,
:headers, :cookies, :network,
:mouse, :keyboard
:mouse, :keyboard, :event, :document_id

def initialize(target_id, browser)
@frames = {}
@target_id, @browser = target_id, browser
@event = Event.new.tap(&:set)

@frames = {}
@waiting_frames ||= Set.new
@frame_stack = []

host = @browser.process.host
port = @browser.process.port
ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
Expand Down Expand Up @@ -135,16 +136,15 @@ def on(name, &block)
private

def subscribe
super

frames_subscribe
network.subscribe

on("Network.loadingFailed") do |params|
id, canceled = params.values_at("requestId", "canceled")
# Set event as we aborted main request we are waiting for
if network.request&.id == id && canceled == true
@event.set
@document_id = get_document_id
get_document_id
end
end

Expand All @@ -165,7 +165,7 @@ def subscribe
# `frameStoppedLoading` doesn't occur if status isn't success
if network.status != 200
@event.set
@document_id = get_document_id
get_document_id
end
end
end
Expand Down Expand Up @@ -198,7 +198,7 @@ def prepare_page
# occurs and thus search for nodes cannot be completed. Here we check
# the history and if the transitionType for example `link` then
# content is already loaded and we can try to get the document.
@document_id = get_document_id
get_document_id
end
end

Expand All @@ -210,7 +210,7 @@ def inject_extensions
# We also evaluate script just in case because
# `Page.addScriptToEvaluateOnNewDocument` doesn't work in popups.
command("Runtime.evaluate", expression: extension,
contextId: execution_context_id,
contextId: execution_id,
returnByValue: true)
end
end
Expand All @@ -237,7 +237,7 @@ def combine_url!(url_or_path)
end

def get_document_id
command("DOM.getDocument", depth: 0).dig("root", "nodeId")
@document_id = command("DOM.getDocument", depth: 0).dig("root", "nodeId")
end
end
end
Loading