Skip to content

Commit 89c0ef3

Browse files
committed
Refactor runtime, add evaluate_async
1 parent 3073a06 commit 89c0ef3

File tree

7 files changed

+292
-270
lines changed

7 files changed

+292
-270
lines changed

lib/ferrum/browser.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class Browser
2424
mouse keyboard
2525
screenshot pdf mhtml viewport_size
2626
frames frame_by main_frame
27-
evaluate evaluate_on evaluate_async execute
27+
evaluate evaluate_on evaluate_async execute evaluate_func
2828
add_script_tag add_style_tag bypass_csp
2929
on goto] => :page
3030
delegate %i[default_user_agent] => :process

lib/ferrum/frame/dom.rb

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,54 +37,54 @@ def body
3737
end
3838

3939
def xpath(selector, within: nil)
40-
code = <<~JS
41-
let selector = arguments[0];
42-
let within = arguments[1] || document;
43-
let results = [];
40+
expr = <<~JS
41+
function(selector, within) {
42+
let results = [];
43+
within ||= document
4444
45-
let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
46-
for (let i = 0; i < xpath.snapshotLength; i++) {
47-
results.push(xpath.snapshotItem(i));
48-
}
45+
let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
46+
for (let i = 0; i < xpath.snapshotLength; i++) {
47+
results.push(xpath.snapshotItem(i));
48+
}
4949
50-
arguments[2](results);
50+
return results;
51+
}
5152
JS
5253

53-
evaluate_async(code, @page.timeout, selector, within)
54+
evaluate_func(expr, selector, within)
5455
end
5556

5657
def at_xpath(selector, within: nil)
57-
code = <<~JS
58-
let selector = arguments[0];
59-
let within = arguments[1] || document;
60-
let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
61-
let result = xpath.snapshotItem(0);
62-
arguments[2](result);
58+
expr = <<~JS
59+
function(selector, within) {
60+
within ||= document
61+
let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
62+
return xpath.snapshotItem(0);
63+
}
6364
JS
64-
65-
evaluate_async(code, @page.timeout, selector, within)
65+
evaluate_func(expr, selector, within)
6666
end
6767

6868
def css(selector, within: nil)
69-
code = <<~JS
70-
let selector = arguments[0];
71-
let within = arguments[1] || document;
72-
let results = Array.from(within.querySelectorAll(selector));
73-
arguments[2](results);
69+
expr = <<~JS
70+
function(selector, within) {
71+
within ||= document
72+
return Array.from(within.querySelectorAll(selector));
73+
}
7474
JS
7575

76-
evaluate_async(code, @page.timeout, selector, within)
76+
evaluate_func(expr, selector, within)
7777
end
7878

7979
def at_css(selector, within: nil)
80-
code = <<~JS
81-
let selector = arguments[0];
82-
let within = arguments[1] || document;
83-
let result = within.querySelector(selector);
84-
arguments[2](result);
80+
expr = <<~JS
81+
function(selector, within) {
82+
within ||= document
83+
return within.querySelector(selector);
84+
}
8585
JS
8686

87-
evaluate_async(code, @page.timeout, selector, within)
87+
evaluate_func(expr, selector, within)
8888
end
8989
end
9090
end

lib/ferrum/frame/runtime.rb

Lines changed: 53 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,19 @@
33
require "singleton"
44

55
module Ferrum
6+
class CyclicObject
7+
include Singleton
8+
9+
def inspect
10+
%(#<#{self.class} JavaScript object that cannot be represented in Ruby>)
11+
end
12+
end
13+
614
class Frame
715
module Runtime
816
INTERMITTENT_ATTEMPTS = ENV.fetch("FERRUM_INTERMITTENT_ATTEMPTS", 6).to_i
917
INTERMITTENT_SLEEP = ENV.fetch("FERRUM_INTERMITTENT_SLEEP", 0.1).to_f
1018

11-
EXECUTE_OPTIONS = {
12-
returnByValue: true,
13-
functionDeclaration: %(function() { %s })
14-
}.freeze
15-
DEFAULT_OPTIONS = {
16-
functionDeclaration: %(function() { return %s })
17-
}.freeze
18-
EVALUATE_ASYNC_OPTIONS = {
19-
awaitPromise: true,
20-
functionDeclaration: %(
21-
function() {
22-
return new Promise((__resolve, __reject) => {
23-
try {
24-
arguments[arguments.length] = r => __resolve(r);
25-
arguments.length = arguments.length + 1;
26-
setTimeout(() => __reject(new Error("timed out promise")), %s);
27-
%s
28-
} catch(error) {
29-
__reject(error);
30-
}
31-
});
32-
}
33-
)
34-
}.freeze
35-
3619
SCRIPT_SRC_TAG = <<~JS
3720
const script = document.createElement("script");
3821
script.src = arguments[0];
@@ -63,37 +46,45 @@ module Runtime
6346
JS
6447

6548
def evaluate(expression, *args)
66-
call(*args, expression: expression)
49+
expression = "function() { return %s }" % expression
50+
call(expression: expression, arguments: args)
6751
end
6852

69-
def evaluate_async(expression, wait_time, *args)
70-
call(*args, expression: expression, wait_time: wait_time * 1000, **EVALUATE_ASYNC_OPTIONS)
53+
def evaluate_async(expression, wait, *args)
54+
template = <<~JS
55+
function() {
56+
return new Promise((__f, __r) => {
57+
try {
58+
arguments[arguments.length] = r => __f(r);
59+
arguments.length = arguments.length + 1;
60+
setTimeout(() => __r(new Error("timed out promise")), %s);
61+
%s
62+
} catch(error) {
63+
__r(error);
64+
}
65+
});
66+
}
67+
JS
68+
69+
expression = template % [wait * 1000, expression]
70+
call(expression: expression, arguments: args, awaitPromise: true)
7171
end
7272

7373
def execute(expression, *args)
74-
call(*args, expression: expression, handle: false, **EXECUTE_OPTIONS)
74+
expression = "function() { %s }" % expression
75+
call(expression: expression, arguments: args, handle: false, returnByValue: true)
7576
true
7677
end
7778

78-
def evaluate_on(node:, expression:, by_value: true, wait: 0)
79-
errors = [NodeNotFoundError, NoExecutionContextError]
80-
attempts, sleep = INTERMITTENT_ATTEMPTS, INTERMITTENT_SLEEP
81-
82-
Ferrum.with_attempts(errors: errors, max: attempts, wait: sleep) do
83-
response = @page.command("DOM.resolveNode", nodeId: node.node_id)
84-
object_id = response.dig("object", "objectId")
85-
options = DEFAULT_OPTIONS.merge(objectId: object_id)
86-
options[:functionDeclaration] = options[:functionDeclaration] % expression
87-
options.merge!(returnByValue: by_value)
88-
89-
response = @page.command("Runtime.callFunctionOn",
90-
wait: wait, slowmoable: true,
91-
**options)
92-
handle_error(response)
93-
response = response["result"]
79+
def evaluate_func(expression, *args, on: nil)
80+
call(expression: expression, arguments: args, on: on)
81+
end
9482

95-
by_value ? response.dig("value") : handle_response(response)
96-
end
83+
def evaluate_on(node:, expression:, by_value: true, wait: 0)
84+
options = { handle: true }
85+
expression = "function() { return %s }" % expression
86+
options = { handle: false, returnByValue: true } if by_value
87+
call(expression: expression, on: node, wait: wait, **options)
9788
end
9889

9990
def add_script_tag(url: nil, path: nil, content: nil, type: "text/javascript")
@@ -126,27 +117,32 @@ def add_style_tag(url: nil, path: nil, content: nil)
126117

127118
private
128119

129-
def call(*args, expression:, wait_time: nil, handle: true, **options)
120+
def call(expression:, arguments: [], on: nil, wait: 0, handle: true, **options)
121+
params = options.dup
130122
errors = [NodeNotFoundError, NoExecutionContextError]
131123
attempts, sleep = INTERMITTENT_ATTEMPTS, INTERMITTENT_SLEEP
132124

133125
Ferrum.with_attempts(errors: errors, max: attempts, wait: sleep) do
134-
arguments = prepare_args(args)
135-
params = DEFAULT_OPTIONS.merge(options)
136-
expression = [wait_time, expression] if wait_time
137-
params[:functionDeclaration] = params[:functionDeclaration] % expression
138-
params = params.merge(arguments: arguments)
139-
unless params[:executionContextId]
140-
params = params.merge(executionContextId: execution_id)
126+
if on
127+
response = @page.command("DOM.resolveNode", nodeId: on.node_id)
128+
object_id = response.dig("object", "objectId")
129+
params.merge!(objectId: object_id)
130+
elsif params[:executionContextId].nil?
131+
params.merge!(executionContextId: execution_id)
132+
else
133+
# executionContextId is passed, nop
141134
end
142135

136+
params.merge!(functionDeclaration: expression,
137+
arguments: prepare_args(arguments))
138+
143139
response = @page.command("Runtime.callFunctionOn",
144-
slowmoable: true,
140+
wait: wait, slowmoable: true,
145141
**params)
146142
handle_error(response)
147143
response = response["result"]
148144

149-
handle ? handle_response(response) : response
145+
handle ? handle_response(response) : response.dig("value")
150146
end
151147
end
152148

@@ -265,12 +261,4 @@ def cyclic_object
265261
end
266262
end
267263
end
268-
269-
class CyclicObject
270-
include Singleton
271-
272-
def inspect
273-
%(#<#{self.class} JavaScript object that cannot be represented in Ruby>)
274-
end
275-
end
276264
end

lib/ferrum/page.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def reset
3232
extend Forwardable
3333
delegate %i[at_css at_xpath css xpath
3434
current_url current_title url title body doctype set_content
35-
execution_id evaluate evaluate_on evaluate_async execute
35+
execution_id evaluate evaluate_on evaluate_async execute evaluate_func
3636
add_script_tag add_style_tag] => :main_frame
3737

3838
include Frames, Screenshot

spec/browser_spec.rb

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,6 @@ module Ferrum
143143
expect(browser.body).to include("x: 100, y: 150")
144144
end
145145

146-
it "supports executing multiple lines of javascript" do
147-
browser.execute <<-JS
148-
var a = 1
149-
var b = 2
150-
window.result = a + b
151-
JS
152-
expect(browser.evaluate("window.result")).to eq(3)
153-
end
154-
155146
it "supports stopping the session", skip: Ferrum.windows? do
156147
browser = Browser.new
157148
pid = browser.process.pid
@@ -474,99 +465,6 @@ module Ferrum
474465
expect(value).to be_nil
475466
end
476467

477-
context "evaluate" do
478-
it "can return an element" do
479-
browser.go_to("/ferrum/type")
480-
element = browser.evaluate(%(document.getElementById("empty_input")))
481-
expect(element).to eq(browser.at_css("#empty_input"))
482-
end
483-
484-
it "can return structures with elements" do
485-
browser.go_to("/ferrum/type")
486-
result = browser.evaluate <<~JS
487-
{
488-
a: document.getElementById("empty_input"),
489-
b: { c: document.querySelectorAll("#empty_textarea, #filled_textarea") }
490-
}
491-
JS
492-
493-
expect(result).to eq(
494-
"a" => browser.at_css("#empty_input"),
495-
"b" => {
496-
"c" => browser.css("#empty_textarea, #filled_textarea")
497-
}
498-
)
499-
end
500-
end
501-
502-
context "evaluate_async" do
503-
it "handles evaluate_async value properly" do
504-
expect(browser.evaluate_async("arguments[0](null)", 5)).to be_nil
505-
expect(browser.evaluate_async("arguments[0](false)", 5)).to be false
506-
expect(browser.evaluate_async("arguments[0](true)", 5)).to be true
507-
expect(browser.evaluate_async(%(arguments[0]({foo: "bar"})), 5)).to eq("foo" => "bar")
508-
end
509-
510-
it "will timeout" do
511-
expect {
512-
browser.evaluate_async("var callback=arguments[0]; setTimeout(function(){callback(true)}, 4000)", 1)
513-
}.to raise_error(Ferrum::ScriptTimeoutError)
514-
end
515-
end
516-
517-
it "handles evaluate values properly" do
518-
expect(browser.evaluate("null")).to be_nil
519-
expect(browser.evaluate("false")).to be false
520-
expect(browser.evaluate("true")).to be true
521-
expect(browser.evaluate("undefined")).to eq(nil)
522-
523-
expect(browser.evaluate("3;")).to eq(3)
524-
expect(browser.evaluate("31337")).to eq(31337)
525-
expect(browser.evaluate(%("string"))).to eq("string")
526-
expect(browser.evaluate(%({foo: "bar"}))).to eq("foo" => "bar")
527-
528-
expect(browser.evaluate("new Object")).to eq({})
529-
expect(browser.evaluate("new Date(2012, 0).toDateString()")).to eq("Sun Jan 01 2012")
530-
expect(browser.evaluate("new Object({a: 1})")).to eq({"a" => 1})
531-
expect(browser.evaluate("new Array")).to eq([])
532-
expect(browser.evaluate("new Function")).to eq({})
533-
534-
expect {
535-
browser.evaluate(%(throw "smth"))
536-
}.to raise_error(Ferrum::JavaScriptError)
537-
end
538-
539-
context "cyclic structure" do
540-
context "ignores seen" do
541-
let(:code) {
542-
<<~JS
543-
(function() {
544-
var a = {};
545-
var b = {};
546-
var c = {};
547-
c.a = a;
548-
a.a = a;
549-
a.b = b;
550-
a.c = c;
551-
return %s;
552-
})()
553-
JS
554-
}
555-
556-
it "objects" do
557-
expect(browser.evaluate(code % "a")).to eq(CyclicObject.instance)
558-
end
559-
560-
it "arrays" do
561-
expect(browser.evaluate(code % "[a]")).to eq([CyclicObject.instance])
562-
end
563-
end
564-
565-
it "backtracks what it has seen" do
566-
expect(browser.evaluate("(function() { var a = {}; return [a, a] })()")).to eq([{}, {}])
567-
end
568-
end
569-
570468
it "synchronizes page loads properly" do
571469
browser.go_to("/ferrum/index")
572470
browser.at_xpath("//a[text() = 'JS redirect']").click

0 commit comments

Comments
 (0)