cuprite 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +100 -67
- data/lib/capybara/cuprite/browser.rb +66 -224
- data/lib/capybara/cuprite/driver.rb +101 -48
- data/lib/capybara/cuprite/errors.rb +1 -64
- data/lib/capybara/cuprite/{browser/javascripts → javascripts}/index.js +26 -20
- data/lib/capybara/cuprite/node.rb +37 -31
- data/lib/capybara/cuprite/page.rb +166 -0
- data/lib/capybara/cuprite/version.rb +1 -1
- data/lib/capybara/cuprite.rb +8 -21
- metadata +8 -42
- data/lib/capybara/cuprite/browser/client.rb +0 -74
- data/lib/capybara/cuprite/browser/dom.rb +0 -50
- data/lib/capybara/cuprite/browser/frame.rb +0 -115
- data/lib/capybara/cuprite/browser/input.json +0 -1341
- data/lib/capybara/cuprite/browser/input.rb +0 -200
- data/lib/capybara/cuprite/browser/net.rb +0 -90
- data/lib/capybara/cuprite/browser/page.rb +0 -378
- data/lib/capybara/cuprite/browser/process.rb +0 -223
- data/lib/capybara/cuprite/browser/runtime.rb +0 -182
- data/lib/capybara/cuprite/browser/targets.rb +0 -129
- data/lib/capybara/cuprite/browser/web_socket.rb +0 -69
- data/lib/capybara/cuprite/network/error.rb +0 -25
- data/lib/capybara/cuprite/network/request.rb +0 -33
- data/lib/capybara/cuprite/network/response.rb +0 -44
@@ -1,200 +0,0 @@
|
|
1
|
-
module Capybara::Cuprite
|
2
|
-
class Browser
|
3
|
-
module Input
|
4
|
-
KEYS = JSON.parse(File.read(File.expand_path("../input.json", __FILE__)))
|
5
|
-
MODIFIERS = { "alt" => 1, "ctrl" => 2, "control" => 2, "meta" => 4, "command" => 4, "shift" => 8 }
|
6
|
-
KEYS_MAPPING = {
|
7
|
-
cancel: "Cancel", help: "Help", backspace: "Backspace", tab: "Tab",
|
8
|
-
clear: "Clear", return: "Enter", enter: "Enter", shift: "Shift",
|
9
|
-
ctrl: "Control", control: "Control", alt: "Alt", pause: "Pause",
|
10
|
-
escape: "Escape", space: "Space", pageup: "PageUp", page_up: "PageUp",
|
11
|
-
pagedown: "PageDown", page_down: "PageDown", end: "End", home: "Home",
|
12
|
-
left: "ArrowLeft", up: "ArrowUp", right: "ArrowRight",
|
13
|
-
down: "ArrowDown", insert: "Insert", delete: "Delete",
|
14
|
-
semicolon: "Semicolon", equals: "Equal", numpad0: "Numpad0",
|
15
|
-
numpad1: "Numpad1", numpad2: "Numpad2", numpad3: "Numpad3",
|
16
|
-
numpad4: "Numpad4", numpad5: "Numpad5", numpad6: "Numpad6",
|
17
|
-
numpad7: "Numpad7", numpad8: "Numpad8", numpad9: "Numpad9",
|
18
|
-
multiply: "NumpadMultiply", add: "NumpadAdd",
|
19
|
-
separator: "NumpadDecimal", subtract: "NumpadSubtract",
|
20
|
-
decimal: "NumpadDecimal", divide: "NumpadDivide", f1: "F1", f2: "F2",
|
21
|
-
f3: "F3", f4: "F4", f5: "F5", f6: "F6", f7: "F7", f8: "F8", f9: "F9",
|
22
|
-
f10: "F10", f11: "F11", f12: "F12", meta: "Meta", command: "Meta",
|
23
|
-
}
|
24
|
-
|
25
|
-
def click(node, keys = [], offset = {})
|
26
|
-
x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
|
27
|
-
command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 1)
|
28
|
-
@wait = 0.05 # Potential wait because if network event is triggered then we have to wait until it's over.
|
29
|
-
command("Input.dispatchMouseEvent", type: "mouseReleased", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 1)
|
30
|
-
end
|
31
|
-
|
32
|
-
def right_click(node, keys = [], offset = {})
|
33
|
-
x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
|
34
|
-
command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "right", x: x, y: y, clickCount: 1)
|
35
|
-
command("Input.dispatchMouseEvent", type: "mouseReleased", modifiers: modifiers, button: "right", x: x, y: y, clickCount: 1)
|
36
|
-
end
|
37
|
-
|
38
|
-
def double_click(node, keys = [], offset = {})
|
39
|
-
x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
|
40
|
-
command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 2)
|
41
|
-
command("Input.dispatchMouseEvent", type: "mouseReleased", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 2)
|
42
|
-
end
|
43
|
-
|
44
|
-
def click_coordinates(x, y)
|
45
|
-
command("Input.dispatchMouseEvent", type: "mousePressed", button: "left", x: x, y: y, clickCount: 1)
|
46
|
-
@wait = 0.05 # Potential wait because if network event is triggered then we have to wait until it's over.
|
47
|
-
command("Input.dispatchMouseEvent", type: "mouseReleased", button: "left", x: x, y: y, clickCount: 1)
|
48
|
-
end
|
49
|
-
|
50
|
-
def hover(node)
|
51
|
-
evaluate_on(node: node, expr: "_cuprite.scrollIntoViewport(this)")
|
52
|
-
x, y = calculate_quads(node)
|
53
|
-
command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y)
|
54
|
-
end
|
55
|
-
|
56
|
-
def set(node, value)
|
57
|
-
object_id = command("DOM.resolveNode", nodeId: node["nodeId"]).dig("object", "objectId")
|
58
|
-
evaluate("_cuprite.set(arguments[0], arguments[1])", { "objectId" => object_id }, value)
|
59
|
-
end
|
60
|
-
|
61
|
-
def drag(node, other)
|
62
|
-
raise NotImplementedError
|
63
|
-
end
|
64
|
-
|
65
|
-
def drag_by(node, x, y)
|
66
|
-
raise NotImplementedError
|
67
|
-
end
|
68
|
-
|
69
|
-
def select(node, value)
|
70
|
-
evaluate_on(node: node, expr: "_cuprite.select(this, #{value})")
|
71
|
-
end
|
72
|
-
|
73
|
-
def trigger(node, event)
|
74
|
-
options = event.to_s == "click" ? { wait: 0.1 } : {}
|
75
|
-
evaluate_on(node: node, expr: %(_cuprite.trigger(this, "#{event}")), **options)
|
76
|
-
end
|
77
|
-
|
78
|
-
def scroll_to(top, left)
|
79
|
-
execute("window.scrollTo(#{top}, #{left})")
|
80
|
-
end
|
81
|
-
|
82
|
-
def send_keys(node, keys)
|
83
|
-
keys = normalize_keys(Array(keys))
|
84
|
-
|
85
|
-
click(node) if !evaluate_on(node: node, expr: %(_cuprite.containsSelection(this)))
|
86
|
-
|
87
|
-
keys.each do |key|
|
88
|
-
type = key[:text] ? "keyDown" : "rawKeyDown"
|
89
|
-
command("Input.dispatchKeyEvent", type: type, **key)
|
90
|
-
command("Input.dispatchKeyEvent", type: "keyUp", **key)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def normalize_keys(keys, pressed_keys = [], memo = [])
|
95
|
-
case keys
|
96
|
-
when Array
|
97
|
-
pressed_keys.push([])
|
98
|
-
memo += combine_strings(keys).map { |k| normalize_keys(k, pressed_keys, memo) }
|
99
|
-
pressed_keys.pop
|
100
|
-
memo.flatten.compact
|
101
|
-
when Symbol
|
102
|
-
key = keys.to_s.downcase
|
103
|
-
|
104
|
-
if MODIFIERS.keys.include?(key)
|
105
|
-
pressed_keys.last.push(key)
|
106
|
-
nil
|
107
|
-
else
|
108
|
-
_key = KEYS.fetch(KEYS_MAPPING[key.to_sym] || key.to_sym)
|
109
|
-
_key[:modifiers] = pressed_keys.flatten.map { |k| MODIFIERS[k] }.reduce(0, :|)
|
110
|
-
to_options(_key)
|
111
|
-
end
|
112
|
-
when String
|
113
|
-
pressed = pressed_keys.flatten
|
114
|
-
keys.each_char.map do |char|
|
115
|
-
if pressed.empty?
|
116
|
-
key = KEYS[char] || {}
|
117
|
-
key = key.merge(text: char, unmodifiedText: char)
|
118
|
-
[to_options(key)]
|
119
|
-
else
|
120
|
-
key = KEYS[char] || {}
|
121
|
-
text = pressed == ["shift"] ? char.upcase : char
|
122
|
-
key = key.merge(
|
123
|
-
text: text,
|
124
|
-
unmodifiedText: text,
|
125
|
-
isKeypad: key["location"] == 3,
|
126
|
-
modifiers: pressed.map { |k| MODIFIERS[k] }.reduce(0, :|),
|
127
|
-
)
|
128
|
-
|
129
|
-
modifiers = pressed.map { |k| to_options(KEYS.fetch(KEYS_MAPPING[k.to_sym])) }
|
130
|
-
modifiers + [to_options(key)]
|
131
|
-
end.flatten
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def combine_strings(keys)
|
137
|
-
keys
|
138
|
-
.chunk { |k| k.is_a?(String) }
|
139
|
-
.map { |s, k| s ? [k.reduce(&:+)] : k }
|
140
|
-
.reduce(&:+)
|
141
|
-
end
|
142
|
-
|
143
|
-
private
|
144
|
-
|
145
|
-
def prepare_before_click(name, node, keys, offset)
|
146
|
-
evaluate_on(node: node, expr: "_cuprite.scrollIntoViewport(this)")
|
147
|
-
x, y = calculate_quads(node, offset[:x], offset[:y])
|
148
|
-
evaluate_on(node: node, expr: "_cuprite.mouseEventTest(this, '#{name}', #{x}, #{y})")
|
149
|
-
|
150
|
-
modifiers = keys.map { |k| MODIFIERS[k.to_s] }.compact.reduce(0, :|)
|
151
|
-
|
152
|
-
command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y)
|
153
|
-
|
154
|
-
[x, y, modifiers]
|
155
|
-
end
|
156
|
-
|
157
|
-
def calculate_quads(node, offset_x = nil, offset_y = nil)
|
158
|
-
quads = get_content_quads(node)
|
159
|
-
offset_x, offset_y = offset_x.to_i, offset_y.to_i
|
160
|
-
|
161
|
-
if offset_x > 0 || offset_y > 0
|
162
|
-
point = quads.first
|
163
|
-
[point[:x] + offset_x, point[:y] + offset_y]
|
164
|
-
else
|
165
|
-
x, y = quads.inject([0, 0]) do |memo, point|
|
166
|
-
[memo[0] + point[:x],
|
167
|
-
memo[1] + point[:y]]
|
168
|
-
end
|
169
|
-
[x / 4, y / 4]
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
def get_content_quads(node)
|
174
|
-
begin
|
175
|
-
result = command("DOM.getContentQuads", nodeId: node["nodeId"])
|
176
|
-
rescue BrowserError => e
|
177
|
-
if e.message == "Could not compute content quads."
|
178
|
-
raise MouseEventFailed.new("MouseEventFailed: click, none, 0, 0")
|
179
|
-
else
|
180
|
-
raise
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
raise "Node is either not visible or not an HTMLElement" if result["quads"].size == 0
|
185
|
-
|
186
|
-
# FIXME: Case when a few quads returned
|
187
|
-
result["quads"].map do |quad|
|
188
|
-
[{x: quad[0], y: quad[1]},
|
189
|
-
{x: quad[2], y: quad[3]},
|
190
|
-
{x: quad[4], y: quad[5]},
|
191
|
-
{x: quad[6], y: quad[7]}]
|
192
|
-
end.first
|
193
|
-
end
|
194
|
-
|
195
|
-
def to_options(hash)
|
196
|
-
hash.inject({}) { |memo, (k, v)| memo.merge(k.to_sym => v) }
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
@@ -1,90 +0,0 @@
|
|
1
|
-
module Capybara::Cuprite
|
2
|
-
class Browser
|
3
|
-
module Net
|
4
|
-
def proxy_authorize(user, password)
|
5
|
-
if user && password
|
6
|
-
@proxy_username, @proxy_password = user, password
|
7
|
-
intercept_request("*")
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
def authorize(user, password)
|
12
|
-
@username, @password = user, password
|
13
|
-
intercept_request("*")
|
14
|
-
end
|
15
|
-
|
16
|
-
def intercept_request(patterns)
|
17
|
-
patterns = Array(patterns).map { |p| { urlPattern: p } }
|
18
|
-
@client.command("Network.setRequestInterception", patterns: patterns)
|
19
|
-
end
|
20
|
-
|
21
|
-
def continue_request(interception_id, options = nil)
|
22
|
-
options ||= {}
|
23
|
-
options = options.merge(interceptionId: interception_id)
|
24
|
-
@client.command("Network.continueInterceptedRequest", **options)
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def subscribe_events
|
30
|
-
super if defined?(super)
|
31
|
-
|
32
|
-
@client.subscribe("Network.loadingFailed") do |params|
|
33
|
-
# Free mutex as we aborted main request we are waiting for
|
34
|
-
if params["requestId"] == @request_id && params["canceled"] == true
|
35
|
-
signal
|
36
|
-
@client.command("DOM.getDocument", depth: 0)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
@client.subscribe("Network.requestIntercepted") do |params|
|
41
|
-
@authorized_ids ||= []
|
42
|
-
@proxy_authorized_ids ||= []
|
43
|
-
url = params.dig("request", "url")
|
44
|
-
interception_id = params["interceptionId"]
|
45
|
-
|
46
|
-
if params["authChallenge"]
|
47
|
-
response = if params.dig("authChallenge", "source") == "Proxy"
|
48
|
-
if @proxy_authorized_ids.include?(interception_id)
|
49
|
-
{ response: "CancelAuth" }
|
50
|
-
elsif @proxy_username && @proxy_password
|
51
|
-
{ response: "ProvideCredentials",
|
52
|
-
username: @proxy_username,
|
53
|
-
password: @proxy_password }
|
54
|
-
else
|
55
|
-
{ response: "CancelAuth" }
|
56
|
-
end
|
57
|
-
else
|
58
|
-
if @authorized_ids.include?(interception_id)
|
59
|
-
{ response: "CancelAuth" }
|
60
|
-
elsif @username && @password
|
61
|
-
{ response: "ProvideCredentials",
|
62
|
-
username: @username,
|
63
|
-
password: @password }
|
64
|
-
else
|
65
|
-
{ response: "CancelAuth" }
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
@authorized_ids << interception_id
|
70
|
-
continue_request(interception_id, authChallengeResponse: response)
|
71
|
-
elsif @browser.url_blacklist && !@browser.url_blacklist.empty?
|
72
|
-
if @browser.url_blacklist.any? { |r| r.match(url) }
|
73
|
-
continue_request(interception_id, errorReason: "Aborted")
|
74
|
-
else
|
75
|
-
continue_request(interception_id)
|
76
|
-
end
|
77
|
-
elsif @browser.url_whitelist && !@browser.url_whitelist.empty?
|
78
|
-
if @browser.url_whitelist.any? { |r| r.match(url) }
|
79
|
-
continue_request(interception_id)
|
80
|
-
else
|
81
|
-
continue_request(interception_id, errorReason: "Aborted")
|
82
|
-
end
|
83
|
-
else
|
84
|
-
continue_request(interception_id)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
@@ -1,378 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "capybara/cuprite/browser/dom"
|
4
|
-
require "capybara/cuprite/browser/input"
|
5
|
-
require "capybara/cuprite/browser/runtime"
|
6
|
-
require "capybara/cuprite/browser/frame"
|
7
|
-
require "capybara/cuprite/browser/client"
|
8
|
-
require "capybara/cuprite/browser/net"
|
9
|
-
require "capybara/cuprite/network/error"
|
10
|
-
require "capybara/cuprite/network/request"
|
11
|
-
require "capybara/cuprite/network/response"
|
12
|
-
|
13
|
-
# RemoteObjectId is from a JavaScript world, and corresponds to any JavaScript
|
14
|
-
# object, including JS wrappers for DOM nodes. There is a way to convert between
|
15
|
-
# node ids and remote object ids (DOM.requestNode and DOM.resolveNode).
|
16
|
-
#
|
17
|
-
# NodeId is used for inspection, when backend tracks the node and sends updates to
|
18
|
-
# the frontend. If you somehow got NodeId over protocol, backend should have
|
19
|
-
# pushed to the frontend all of it's ancestors up to the Document node via
|
20
|
-
# DOM.setChildNodes. After that, frontend is always kept up-to-date about anything
|
21
|
-
# happening to the node.
|
22
|
-
#
|
23
|
-
# BackendNodeId is just a unique identifier for a node. Obtaining it does not send
|
24
|
-
# any updates, for example, the node may be destroyed without any notification.
|
25
|
-
# This is a way to keep a reference to the Node, when you don't necessarily want
|
26
|
-
# to keep track of it. One example would be linking to the node from performance
|
27
|
-
# data (e.g. relayout root node). BackendNodeId may be either resolved to
|
28
|
-
# inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
|
29
|
-
# details (DOM.describeNode).
|
30
|
-
module Capybara::Cuprite
|
31
|
-
class Browser
|
32
|
-
class Page
|
33
|
-
include Input, DOM, Runtime, Frame, Net
|
34
|
-
|
35
|
-
attr_accessor :referrer
|
36
|
-
attr_reader :target_id, :status_code, :response_headers
|
37
|
-
|
38
|
-
def initialize(target_id, browser)
|
39
|
-
@wait = 0
|
40
|
-
@target_id, @browser = target_id, browser
|
41
|
-
@mutex, @resource = Mutex.new, ConditionVariable.new
|
42
|
-
@network_traffic = []
|
43
|
-
|
44
|
-
@frames = {}
|
45
|
-
@waiting_frames ||= Set.new
|
46
|
-
@frame_stack = []
|
47
|
-
@accept_modal = []
|
48
|
-
@modal_messages = []
|
49
|
-
|
50
|
-
begin
|
51
|
-
@session_id = @browser.command("Target.attachToTarget", targetId: @target_id)["sessionId"]
|
52
|
-
rescue BrowserError => e
|
53
|
-
if e.message == "No target with given id found"
|
54
|
-
raise NoSuchWindowError
|
55
|
-
else
|
56
|
-
raise
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
host = @browser.process.host
|
61
|
-
port = @browser.process.port
|
62
|
-
ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
|
63
|
-
@client = Client.new(browser, ws_url)
|
64
|
-
|
65
|
-
subscribe_events
|
66
|
-
prepare_page
|
67
|
-
end
|
68
|
-
|
69
|
-
def timeout
|
70
|
-
@browser.timeout
|
71
|
-
end
|
72
|
-
|
73
|
-
def visit(url)
|
74
|
-
@wait = timeout
|
75
|
-
options = { url: url }
|
76
|
-
options.merge!(referrer: referrer) if referrer
|
77
|
-
response = command("Page.navigate", **options)
|
78
|
-
# https://cs.chromium.org/chromium/src/net/base/net_error_list.h
|
79
|
-
if %w[net::ERR_NAME_NOT_RESOLVED
|
80
|
-
net::ERR_NAME_RESOLUTION_FAILED
|
81
|
-
net::ERR_INTERNET_DISCONNECTED
|
82
|
-
net::ERR_CONNECTION_TIMED_OUT].include?(response["errorText"])
|
83
|
-
raise StatusFailError, "url" => url
|
84
|
-
end
|
85
|
-
response["frameId"]
|
86
|
-
end
|
87
|
-
|
88
|
-
def close
|
89
|
-
@browser.command("Target.detachFromTarget", sessionId: @session_id)
|
90
|
-
@browser.command("Target.closeTarget", targetId: @target_id)
|
91
|
-
close_connection
|
92
|
-
end
|
93
|
-
|
94
|
-
def close_connection
|
95
|
-
@client.close
|
96
|
-
end
|
97
|
-
|
98
|
-
def resize(width: nil, height: nil, fullscreen: false)
|
99
|
-
result = @browser.command("Browser.getWindowForTarget", targetId: @target_id)
|
100
|
-
@window_id, @bounds = result.values_at("windowId", "bounds")
|
101
|
-
|
102
|
-
if fullscreen
|
103
|
-
@browser.command("Browser.setWindowBounds", windowId: @window_id, bounds: { windowState: "fullscreen" })
|
104
|
-
else
|
105
|
-
@browser.command("Browser.setWindowBounds", windowId: @window_id, bounds: { windowState: "normal" })
|
106
|
-
@browser.command("Browser.setWindowBounds", windowId: @window_id, bounds: { width: width, height: height, windowState: "normal" })
|
107
|
-
command("Emulation.setDeviceMetricsOverride", width: width, height: height, deviceScaleFactor: 1, mobile: false)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def refresh
|
112
|
-
@wait = timeout
|
113
|
-
command("Page.reload")
|
114
|
-
end
|
115
|
-
|
116
|
-
def network_traffic(type = nil)
|
117
|
-
case type.to_s
|
118
|
-
when "all"
|
119
|
-
@network_traffic
|
120
|
-
when "blocked"
|
121
|
-
@network_traffic.select { |r| r.response.nil? } # when request blocked
|
122
|
-
else
|
123
|
-
@network_traffic.select { |r| r.response } # when request isn't blocked
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
def clear_network_traffic
|
128
|
-
@network_traffic = []
|
129
|
-
end
|
130
|
-
|
131
|
-
def go_back
|
132
|
-
go(-1)
|
133
|
-
end
|
134
|
-
|
135
|
-
def go_forward
|
136
|
-
go(1)
|
137
|
-
end
|
138
|
-
|
139
|
-
def accept_confirm
|
140
|
-
@accept_modal << true
|
141
|
-
end
|
142
|
-
|
143
|
-
def dismiss_confirm
|
144
|
-
@accept_modal << false
|
145
|
-
end
|
146
|
-
|
147
|
-
def accept_prompt(modal_response)
|
148
|
-
@accept_modal << true
|
149
|
-
@modal_response = modal_response
|
150
|
-
end
|
151
|
-
|
152
|
-
def dismiss_prompt
|
153
|
-
@accept_modal << false
|
154
|
-
end
|
155
|
-
|
156
|
-
def find_modal(options)
|
157
|
-
start_time = Capybara::Helpers.monotonic_time
|
158
|
-
timeout_sec = options.fetch(:wait) { session_wait_time }
|
159
|
-
expect_text = options[:text]
|
160
|
-
expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s)
|
161
|
-
not_found_msg = "Unable to find modal dialog"
|
162
|
-
not_found_msg += " with #{expect_text}" if expect_text
|
163
|
-
|
164
|
-
begin
|
165
|
-
modal_text = @modal_messages.shift
|
166
|
-
raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
|
167
|
-
rescue Capybara::ModalNotFound => e
|
168
|
-
raise e, not_found_msg if (Capybara::Helpers.monotonic_time - start_time) >= timeout_sec
|
169
|
-
sleep(0.05)
|
170
|
-
retry
|
171
|
-
end
|
172
|
-
|
173
|
-
modal_text
|
174
|
-
end
|
175
|
-
|
176
|
-
def reset_modals
|
177
|
-
@accept_modal = []
|
178
|
-
@modal_response = nil
|
179
|
-
@modal_messages = []
|
180
|
-
end
|
181
|
-
|
182
|
-
def command(*args)
|
183
|
-
id = nil
|
184
|
-
|
185
|
-
@mutex.synchronize do
|
186
|
-
id = @client.command(*args)
|
187
|
-
stop_at = Capybara::Helpers.monotonic_time + @wait
|
188
|
-
|
189
|
-
while @wait > 0 && (remain = stop_at - Capybara::Helpers.monotonic_time) > 0
|
190
|
-
@resource.wait(@mutex, remain)
|
191
|
-
end
|
192
|
-
|
193
|
-
@wait = 0
|
194
|
-
end
|
195
|
-
|
196
|
-
@client.wait(id: id)
|
197
|
-
end
|
198
|
-
|
199
|
-
private
|
200
|
-
|
201
|
-
def subscribe_events
|
202
|
-
super
|
203
|
-
|
204
|
-
if @browser.logger
|
205
|
-
@client.subscribe("Runtime.consoleAPICalled") do |params|
|
206
|
-
params["args"].each { |r| @browser.logger.puts(r["value"]) }
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
if @browser.js_errors
|
211
|
-
@client.subscribe("Runtime.exceptionThrown") do |params|
|
212
|
-
Thread.main.raise JavaScriptError.new(params.dig("exceptionDetails", "exception"))
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
@client.subscribe("Page.javascriptDialogOpening") do |params|
|
217
|
-
accept_modal = @accept_modal.last
|
218
|
-
if accept_modal == true || accept_modal == false
|
219
|
-
@accept_modal.pop
|
220
|
-
@modal_messages << params["message"]
|
221
|
-
options = { accept: accept_modal }
|
222
|
-
response = @modal_response || params["defaultPrompt"]
|
223
|
-
options.merge!(promptText: response) if response
|
224
|
-
@client.command("Page.handleJavaScriptDialog", **options)
|
225
|
-
else
|
226
|
-
warn "Modal window has been opened, but you didn't wrap your code into (`accept_prompt` | `dismiss_prompt` | `accept_confirm` | `dismiss_confirm` | `accept_alert`), accepting by default"
|
227
|
-
options = { accept: true }
|
228
|
-
response = params["defaultPrompt"]
|
229
|
-
options.merge!(promptText: response) if response
|
230
|
-
@client.command("Page.handleJavaScriptDialog", **options)
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
@client.subscribe("Page.windowOpen") do
|
235
|
-
@browser.targets.refresh
|
236
|
-
@mutex.try_lock
|
237
|
-
sleep 0.3 # Dirty hack because new window doesn't have events at all
|
238
|
-
@mutex.unlock if @mutex.locked? && @mutex.owned?
|
239
|
-
end
|
240
|
-
|
241
|
-
@client.subscribe("Page.navigatedWithinDocument") do
|
242
|
-
signal if @waiting_frames.empty?
|
243
|
-
end
|
244
|
-
|
245
|
-
@client.subscribe("Page.domContentEventFired") do |params|
|
246
|
-
# `frameStoppedLoading` doesn't occur if status isn't success
|
247
|
-
if @status_code != 200
|
248
|
-
signal
|
249
|
-
@client.command("DOM.getDocument", depth: 0)
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
@client.subscribe("Network.requestWillBeSent") do |params|
|
254
|
-
if params["frameId"] == @frame_id
|
255
|
-
# Possible types:
|
256
|
-
# Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR,
|
257
|
-
# Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping,
|
258
|
-
# CSPViolationReport, Other
|
259
|
-
if params["type"] == "Document"
|
260
|
-
@mutex.try_lock
|
261
|
-
@request_id = params["requestId"]
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
|
-
id, time = params.values_at("requestId", "wallTime")
|
266
|
-
params = params["request"].merge("id" => id, "time" => time)
|
267
|
-
@network_traffic << Network::Request.new(params)
|
268
|
-
end
|
269
|
-
|
270
|
-
@client.subscribe("Network.responseReceived") do |params|
|
271
|
-
if params["requestId"] == @request_id
|
272
|
-
@response_headers = params.dig("response", "headers")
|
273
|
-
@status_code = params.dig("response", "status")
|
274
|
-
end
|
275
|
-
|
276
|
-
if request = @network_traffic.find { |r| r.id == params["requestId"] }
|
277
|
-
params = params["response"].merge("id" => params["requestId"])
|
278
|
-
request.response = Network::Response.new(params)
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
@client.subscribe("Network.loadingFinished") do |params|
|
283
|
-
if request = @network_traffic.find { |r| r.id == params["requestId"] }
|
284
|
-
# Sometimes we never get the Network.responseReceived event.
|
285
|
-
# See https://crbug.com/883475
|
286
|
-
#
|
287
|
-
# Network.loadingFinished's encodedDataLength contains both body and headers
|
288
|
-
# sizes received by wire. See https://crbug.com/764946
|
289
|
-
if response = request.response
|
290
|
-
response.body_size = params["encodedDataLength"] - response.headers_size
|
291
|
-
end
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
@client.subscribe("Log.entryAdded") do |params|
|
296
|
-
source = params.dig("entry", "source")
|
297
|
-
level = params.dig("entry", "level")
|
298
|
-
if source == "network" && level == "error"
|
299
|
-
id = params.dig("entry", "networkRequestId")
|
300
|
-
if request = @network_traffic.find { |r| r.id == id }
|
301
|
-
request.error = Network::Error.new(params["entry"])
|
302
|
-
end
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
def prepare_page
|
308
|
-
command("Page.enable")
|
309
|
-
command("DOM.enable")
|
310
|
-
command("CSS.enable")
|
311
|
-
command("Runtime.enable")
|
312
|
-
command("Log.enable")
|
313
|
-
command("Network.enable")
|
314
|
-
|
315
|
-
if Capybara.save_path
|
316
|
-
command("Page.setDownloadBehavior", behavior: "allow", downloadPath: Capybara.save_path.to_s)
|
317
|
-
end
|
318
|
-
|
319
|
-
@browser.extensions.each do |extension|
|
320
|
-
@client.command("Page.addScriptToEvaluateOnNewDocument", source: extension)
|
321
|
-
end
|
322
|
-
|
323
|
-
inject_extensions
|
324
|
-
|
325
|
-
width, height = @browser.window_size
|
326
|
-
resize(width: width, height: height)
|
327
|
-
|
328
|
-
url_whitelist = Array(@browser.url_whitelist)
|
329
|
-
url_blacklist = Array(@browser.url_blacklist)
|
330
|
-
intercept_request("*") if !url_whitelist.empty? || !url_blacklist.empty?
|
331
|
-
|
332
|
-
response = command("Page.getNavigationHistory")
|
333
|
-
if response.dig("entries", 0, "transitionType") != "typed"
|
334
|
-
# If we create page by clicking links, submiting forms and so on it
|
335
|
-
# opens a new window for which `frameStoppedLoading` event never
|
336
|
-
# occurs and thus search for nodes cannot be completed. Here we check
|
337
|
-
# the history and if the transitionType for example `link` then
|
338
|
-
# content is already loaded and we can try to get the document.
|
339
|
-
@client.command("DOM.getDocument", depth: 0)
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
def inject_extensions
|
344
|
-
@browser.extensions.each do |extension|
|
345
|
-
# https://github.com/GoogleChrome/puppeteer/issues/1443
|
346
|
-
# https://github.com/ChromeDevTools/devtools-protocol/issues/77
|
347
|
-
# https://github.com/cyrus-and/chrome-remote-interface/issues/319
|
348
|
-
# We also evaluate script just in case because
|
349
|
-
# `Page.addScriptToEvaluateOnNewDocument` doesn't work in popups.
|
350
|
-
@client.command("Runtime.evaluate", expression: extension,
|
351
|
-
contextId: execution_context_id,
|
352
|
-
returnByValue: true)
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
def signal
|
357
|
-
@wait = 0
|
358
|
-
|
359
|
-
if @mutex.locked? && @mutex.owned?
|
360
|
-
@resource.signal
|
361
|
-
@mutex.unlock
|
362
|
-
else
|
363
|
-
@mutex.synchronize { @resource.signal }
|
364
|
-
end
|
365
|
-
end
|
366
|
-
|
367
|
-
def go(delta)
|
368
|
-
history = command("Page.getNavigationHistory")
|
369
|
-
index, entries = history.values_at("currentIndex", "entries")
|
370
|
-
|
371
|
-
if entry = entries[index + delta]
|
372
|
-
@wait = 0.05 # Potential wait because of network event
|
373
|
-
command("Page.navigateToHistoryEntry", entryId: entry["id"])
|
374
|
-
end
|
375
|
-
end
|
376
|
-
end
|
377
|
-
end
|
378
|
-
end
|