Skip to content

Commit 00e2846

Browse files
authored
Merge pull request #20 from route/frames
Implement frames
2 parents 903b597 + b84359a commit 00e2846

File tree

12 files changed

+278
-306
lines changed

12 files changed

+278
-306
lines changed

README.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,20 +221,20 @@ browser.xpath("//a[@aria-label='Issues you created']") # => [Node]
221221

222222
#### current_url : `String`
223223

224-
Returns current window location href.
224+
Returns current top window location href.
225225

226226
```ruby
227227
browser.goto("https://google.com/")
228228
browser.current_url # => "https://www.google.com/"
229229
```
230230

231-
#### title : `String`
231+
#### current_title : `String`
232232

233-
Returns current window title
233+
Returns current top window title
234234

235235
```ruby
236236
browser.goto("https://google.com/")
237-
browser.title # => "Google"
237+
browser.current_title # => "Google"
238238
```
239239

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

605605
## Frames
606606

607-
#### frame_url
608-
#### frame_title
609-
#### within_frame(frame, &block)
607+
#### frames
608+
#### main_frame
609+
#### frame_by
610610

611611
Play around inside given frame
612612

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

622620

lib/ferrum/browser.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ class Browser
2121
headers cookies network
2222
mouse keyboard
2323
screenshot pdf viewport_size
24+
frames frame_by main_frame
2425
evaluate evaluate_on evaluate_async execute
25-
frame_url frame_title within_frame
2626
on] => :page
2727

2828
attr_reader :client, :process, :contexts, :logger, :js_errors,

lib/ferrum/frame.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
require "ferrum/frame/dom"
4+
require "ferrum/frame/runtime"
5+
6+
module Ferrum
7+
class Frame
8+
include DOM, Runtime
9+
10+
attr_reader :id, :parent_id, :page, :state, :name
11+
attr_writer :execution_id
12+
13+
def initialize(page, id, parent_id = nil)
14+
@page, @id, @parent_id = page, id, parent_id
15+
end
16+
17+
# Can be one of:
18+
# * started_loading
19+
# * navigated
20+
# * scheduled_navigation
21+
# * cleared_scheduled_navigation
22+
# * stopped_loading
23+
def state=(value)
24+
@state = value
25+
end
26+
27+
def url
28+
evaluate("document.location.href")
29+
end
30+
31+
def title
32+
evaluate("document.title")
33+
end
34+
35+
def main?
36+
@parent_id.nil?
37+
end
38+
39+
def execution_id
40+
raise NoExecutionContextError unless @execution_id
41+
@execution_id
42+
rescue NoExecutionContextError
43+
@page.event.reset
44+
@page.event.wait(@page.timeout) ? retry : raise
45+
end
46+
end
47+
end
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
# inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
1919
# details (DOM.describeNode).
2020
module Ferrum
21-
class Page
21+
class Frame
2222
module DOM
2323
def current_url
2424
evaluate("window.top.location.href")
2525
end
2626

27-
def title
27+
def current_title
2828
evaluate("window.top.document.title")
2929
end
3030

@@ -57,22 +57,23 @@ def xpath(selector, within: nil)
5757
} else {
5858
throw error;
5959
}
60-
}), timeout, selector, within)
60+
}), @page.timeout, selector, within)
6161
end
6262

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

66-
ids = command("DOM.querySelectorAll",
67-
nodeId: node_id,
68-
selector: selector)["nodeIds"]
67+
ids = @page.command("DOM.querySelectorAll",
68+
nodeId: node_id,
69+
selector: selector)["nodeIds"]
6970
ids.map { |id| build_node(id) }.compact
7071
end
7172

7273
def at_css(selector, within: nil)
73-
node_id = within&.node_id || @document_id
74+
node_id = within&.node_id || @page.document_id
7475

75-
id = command("DOM.querySelector",
76+
id = @page.command("DOM.querySelector",
7677
nodeId: node_id,
7778
selector: selector)["nodeId"]
7879
build_node(id)
@@ -81,8 +82,8 @@ def at_css(selector, within: nil)
8182
private
8283

8384
def build_node(node_id)
84-
description = command("DOM.describeNode", nodeId: node_id)
85-
Node.new(self, target_id, node_id, description["node"])
85+
description = @page.command("DOM.describeNode", nodeId: node_id)
86+
Node.new(self, @page.target_id, node_id, description["node"])
8687
end
8788
end
8889
end
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
module Ferrum
4-
class Page
4+
class Frame
55
module Runtime
66
INTERMITTENT_ATTEMPTS = ENV.fetch("FERRUM_INTERMITTENT_ATTEMPTS", 6).to_i
77
INTERMITTENT_SLEEP = ENV.fetch("FERRUM_INTERMITTENT_SLEEP", 0.1).to_f
@@ -49,13 +49,13 @@ def evaluate_on(node:, expression:, by_value: true, wait: 0)
4949
attempts, sleep = INTERMITTENT_ATTEMPTS, INTERMITTENT_SLEEP
5050

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

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

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

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

8585
handle ? handle_response(response) : response
@@ -115,9 +115,9 @@ def handle_response(response)
115115
# and node destroyed so we need to retrieve it each time for given id.
116116
# Though we can try to subscribe to `DOM.childNodeRemoved` and
117117
# `DOM.childNodeInserted` in the future.
118-
node_id = command("DOM.requestNode", objectId: object_id)["nodeId"]
119-
description = command("DOM.describeNode", nodeId: node_id)["node"]
120-
Node.new(self, target_id, node_id, description)
118+
node_id = @page.command("DOM.requestNode", objectId: object_id)["nodeId"]
119+
description = @page.command("DOM.describeNode", nodeId: node_id)["node"]
120+
Node.new(self, @page.target_id, node_id, description)
121121
when "array"
122122
reduce_props(object_id, []) do |memo, key, value|
123123
next(memo) unless (Integer(key) rescue nil)
@@ -140,7 +140,7 @@ def handle_response(response)
140140
def prepare_args(args)
141141
args.map do |arg|
142142
if arg.is_a?(Node)
143-
resolved = command("DOM.resolveNode", nodeId: arg.node_id)
143+
resolved = @page.command("DOM.resolveNode", nodeId: arg.node_id)
144144
{ objectId: resolved["object"]["objectId"] }
145145
elsif arg.is_a?(Hash) && arg["objectId"]
146146
{ objectId: arg["objectId"] }
@@ -154,7 +154,7 @@ def reduce_props(object_id, to)
154154
if cyclic?(object_id).dig("result", "value")
155155
return "(cyclic structure)"
156156
else
157-
props = command("Runtime.getProperties", objectId: object_id)
157+
props = @page.command("Runtime.getProperties", objectId: object_id)
158158
props["result"].reduce(to) do |memo, prop|
159159
next(memo) unless prop["enumerable"]
160160
yield(memo, prop["name"], prop["value"])
@@ -163,7 +163,7 @@ def reduce_props(object_id, to)
163163
end
164164

165165
def cyclic?(object_id)
166-
command("Runtime.callFunctionOn",
166+
@page.command("Runtime.callFunctionOn",
167167
objectId: object_id,
168168
returnByValue: true,
169169
functionDeclaration: <<~JS

lib/ferrum/network.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def subscribe
9191
end
9292

9393
exchange = Network::Exchange.new(params)
94-
@exchange = exchange if exchange.navigation_request?(@page.frame_id)
94+
@exchange = exchange if exchange.navigation_request?(@page.main_frame.id)
9595
@traffic << exchange
9696
end
9797

lib/ferrum/node.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ module Ferrum
44
class Node
55
attr_reader :page, :target_id, :node_id, :description, :tag_name
66

7-
def initialize(page, target_id, node_id, description)
8-
@page, @target_id = page, target_id
7+
def initialize(frame, target_id, node_id, description)
8+
@page = frame.page
9+
@target_id = target_id
910
@node_id, @description = node_id, description
1011
@tag_name = description["nodeName"].downcase
1112
end
@@ -14,6 +15,14 @@ def node?
1415
description["nodeType"] == 1 # nodeType: 3, nodeName: "#text" e.g.
1516
end
1617

18+
def frame_id
19+
description["frameId"]
20+
end
21+
22+
def frame
23+
page.frame_by(id: frame_id)
24+
end
25+
1726
def focus
1827
tap { page.command("DOM.focus", nodeId: node_id) }
1928
end

lib/ferrum/page.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
# frozen_string_literal: true
22

3+
require "forwardable"
34
require "ferrum/mouse"
45
require "ferrum/keyboard"
56
require "ferrum/headers"
67
require "ferrum/cookies"
78
require "ferrum/dialog"
89
require "ferrum/network"
9-
require "ferrum/page/dom"
10-
require "ferrum/page/runtime"
11-
require "ferrum/page/frame"
10+
require "ferrum/page/frames"
1211
require "ferrum/page/screenshot"
1312
require "ferrum/browser/client"
1413

@@ -28,21 +27,23 @@ def reset
2827
end
2928
end
3029

31-
include DOM, Runtime, Frame, Screenshot
30+
extend Forwardable
31+
delegate %i[at_css at_xpath css xpath
32+
current_url current_title url title body
33+
execution_id evaluate evaluate_on evaluate_async execute] => :main_frame
34+
35+
include Frames, Screenshot
3236

3337
attr_accessor :referrer
3438
attr_reader :target_id, :browser,
3539
:headers, :cookies, :network,
36-
:mouse, :keyboard
40+
:mouse, :keyboard, :event, :document_id
3741

3842
def initialize(target_id, browser)
43+
@frames = {}
3944
@target_id, @browser = target_id, browser
4045
@event = Event.new.tap(&:set)
4146

42-
@frames = {}
43-
@waiting_frames ||= Set.new
44-
@frame_stack = []
45-
4647
host = @browser.process.host
4748
port = @browser.process.port
4849
ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
@@ -135,16 +136,15 @@ def on(name, &block)
135136
private
136137

137138
def subscribe
138-
super
139-
139+
frames_subscribe
140140
network.subscribe
141141

142142
on("Network.loadingFailed") do |params|
143143
id, canceled = params.values_at("requestId", "canceled")
144144
# Set event as we aborted main request we are waiting for
145145
if network.request&.id == id && canceled == true
146146
@event.set
147-
@document_id = get_document_id
147+
get_document_id
148148
end
149149
end
150150

@@ -165,7 +165,7 @@ def subscribe
165165
# `frameStoppedLoading` doesn't occur if status isn't success
166166
if network.status != 200
167167
@event.set
168-
@document_id = get_document_id
168+
get_document_id
169169
end
170170
end
171171
end
@@ -198,7 +198,7 @@ def prepare_page
198198
# occurs and thus search for nodes cannot be completed. Here we check
199199
# the history and if the transitionType for example `link` then
200200
# content is already loaded and we can try to get the document.
201-
@document_id = get_document_id
201+
get_document_id
202202
end
203203
end
204204

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

239239
def get_document_id
240-
command("DOM.getDocument", depth: 0).dig("root", "nodeId")
240+
@document_id = command("DOM.getDocument", depth: 0).dig("root", "nodeId")
241241
end
242242
end
243243
end

0 commit comments

Comments
 (0)