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,223 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "cliver"
|
4
|
-
|
5
|
-
module Capybara::Cuprite
|
6
|
-
class Browser
|
7
|
-
class Process
|
8
|
-
KILL_TIMEOUT = 2
|
9
|
-
PROCESS_TIMEOUT = 1
|
10
|
-
BROWSER_PATH = ENV["BROWSER_PATH"]
|
11
|
-
BROWSER_HOST = "127.0.0.1"
|
12
|
-
BROWSER_PORT = "0"
|
13
|
-
DEFAULT_OPTIONS = {
|
14
|
-
"headless" => nil,
|
15
|
-
"disable-gpu" => nil,
|
16
|
-
"hide-scrollbars" => nil,
|
17
|
-
"mute-audio" => nil,
|
18
|
-
"enable-automation" => nil,
|
19
|
-
"disable-web-security" => nil,
|
20
|
-
"disable-session-crashed-bubble" => nil,
|
21
|
-
"disable-breakpad" => nil,
|
22
|
-
"disable-sync" => nil,
|
23
|
-
"no-first-run" => nil,
|
24
|
-
"use-mock-keychain" => nil,
|
25
|
-
"keep-alive-for-test" => nil,
|
26
|
-
"disable-popup-blocking" => nil,
|
27
|
-
"disable-extensions" => nil,
|
28
|
-
"disable-hang-monitor" => nil,
|
29
|
-
"disable-features" => "site-per-process,TranslateUI",
|
30
|
-
"disable-translate" => nil,
|
31
|
-
"disable-background-networking" => nil,
|
32
|
-
"enable-features" => "NetworkService,NetworkServiceInProcess",
|
33
|
-
"disable-background-timer-throttling" => nil,
|
34
|
-
"disable-backgrounding-occluded-windows" => nil,
|
35
|
-
"disable-client-side-phishing-detection" => nil,
|
36
|
-
"disable-default-apps" => nil,
|
37
|
-
"disable-dev-shm-usage" => nil,
|
38
|
-
"disable-ipc-flooding-protection" => nil,
|
39
|
-
"disable-prompt-on-repost" => nil,
|
40
|
-
"disable-renderer-backgrounding" => nil,
|
41
|
-
"force-color-profile" => "srgb",
|
42
|
-
"metrics-recording-only" => nil,
|
43
|
-
"safebrowsing-disable-auto-update" => nil,
|
44
|
-
"password-store" => "basic",
|
45
|
-
# Note: --no-sandbox is not needed if you properly setup a user in the container.
|
46
|
-
# https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
|
47
|
-
# "no-sandbox" => nil,
|
48
|
-
}.freeze
|
49
|
-
|
50
|
-
attr_reader :host, :port, :ws_url, :pid, :path, :options, :cmd
|
51
|
-
|
52
|
-
def self.start(*args)
|
53
|
-
new(*args).tap(&:start)
|
54
|
-
end
|
55
|
-
|
56
|
-
def self.process_killer(pid)
|
57
|
-
proc do
|
58
|
-
begin
|
59
|
-
if Capybara::Cuprite.windows?
|
60
|
-
::Process.kill("KILL", pid)
|
61
|
-
else
|
62
|
-
::Process.kill("USR1", pid)
|
63
|
-
start = Capybara::Helpers.monotonic_time
|
64
|
-
while ::Process.wait(pid, ::Process::WNOHANG).nil?
|
65
|
-
sleep 0.05
|
66
|
-
next unless (Capybara::Helpers.monotonic_time - start) > KILL_TIMEOUT
|
67
|
-
::Process.kill("KILL", pid)
|
68
|
-
::Process.wait(pid)
|
69
|
-
break
|
70
|
-
end
|
71
|
-
end
|
72
|
-
rescue Errno::ESRCH, Errno::ECHILD
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def initialize(options)
|
78
|
-
@options = {}
|
79
|
-
|
80
|
-
detect_browser_path(options)
|
81
|
-
|
82
|
-
# Doesn't work on MacOS, so we need to set it by CDP as well
|
83
|
-
@options.merge!("window-size" => options[:window_size].join(","))
|
84
|
-
|
85
|
-
port = options.fetch(:port, BROWSER_PORT)
|
86
|
-
@options.merge!("remote-debugging-port" => port)
|
87
|
-
|
88
|
-
host = options.fetch(:host, BROWSER_HOST)
|
89
|
-
@options.merge!("remote-debugging-address" => host)
|
90
|
-
|
91
|
-
@options.merge!("user-data-dir" => Dir.mktmpdir)
|
92
|
-
|
93
|
-
@options = DEFAULT_OPTIONS.merge(@options)
|
94
|
-
|
95
|
-
unless options.fetch(:headless, true)
|
96
|
-
@options.delete("headless")
|
97
|
-
@options.delete("disable-gpu")
|
98
|
-
end
|
99
|
-
|
100
|
-
@process_timeout = options.fetch(:process_timeout, PROCESS_TIMEOUT)
|
101
|
-
|
102
|
-
@options.merge!(options.fetch(:browser_options, {}))
|
103
|
-
|
104
|
-
@logger = options.fetch(:logger, nil)
|
105
|
-
end
|
106
|
-
|
107
|
-
def start
|
108
|
-
read_io, write_io = IO.pipe
|
109
|
-
process_options = { in: File::NULL }
|
110
|
-
process_options[:pgroup] = true unless Capybara::Cuprite.windows?
|
111
|
-
if Capybara::Cuprite.mri?
|
112
|
-
process_options[:out] = process_options[:err] = write_io
|
113
|
-
end
|
114
|
-
|
115
|
-
redirect_stdout(write_io) do
|
116
|
-
@cmd = [@path] + @options.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
|
117
|
-
@pid = ::Process.spawn(*@cmd, process_options)
|
118
|
-
ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
|
119
|
-
end
|
120
|
-
|
121
|
-
parse_ws_url(read_io, @process_timeout)
|
122
|
-
ensure
|
123
|
-
close_io(read_io, write_io)
|
124
|
-
end
|
125
|
-
|
126
|
-
def stop
|
127
|
-
return unless @pid
|
128
|
-
kill
|
129
|
-
ObjectSpace.undefine_finalizer(self)
|
130
|
-
end
|
131
|
-
|
132
|
-
def restart
|
133
|
-
stop
|
134
|
-
start
|
135
|
-
end
|
136
|
-
|
137
|
-
private
|
138
|
-
|
139
|
-
def detect_browser_path(options)
|
140
|
-
@path =
|
141
|
-
options[:browser_path] ||
|
142
|
-
BROWSER_PATH || (
|
143
|
-
if RUBY_PLATFORM.include?('darwin')
|
144
|
-
[
|
145
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
146
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
147
|
-
].find { |path| File.exist?(path) }
|
148
|
-
else
|
149
|
-
%w[chromium google-chrome-unstable google-chrome-beta google-chrome chrome chromium-browser google-chrome-stable].reduce(nil) do |path, exe|
|
150
|
-
path = Cliver.detect(exe)
|
151
|
-
break path if path
|
152
|
-
end
|
153
|
-
end
|
154
|
-
)
|
155
|
-
|
156
|
-
unless @path
|
157
|
-
message = "Could not find an executable for chrome. Try to make it " \
|
158
|
-
"available on the PATH or set environment varible for " \
|
159
|
-
"example BROWSER_PATH=\"/Applications/Chromium.app/Contents/MacOS/Chromium\""
|
160
|
-
raise Cliver::Dependency::NotFound.new(message)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
def redirect_stdout(write_io)
|
165
|
-
if Capybara::Cuprite.mri?
|
166
|
-
yield
|
167
|
-
else
|
168
|
-
begin
|
169
|
-
prev = STDOUT.dup
|
170
|
-
$stdout = write_io
|
171
|
-
STDOUT.reopen(write_io)
|
172
|
-
yield
|
173
|
-
ensure
|
174
|
-
STDOUT.reopen(prev)
|
175
|
-
$stdout = STDOUT
|
176
|
-
prev.close
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
def kill
|
182
|
-
self.class.process_killer(@pid).call
|
183
|
-
@pid = nil
|
184
|
-
end
|
185
|
-
|
186
|
-
def parse_ws_url(read_io, timeout = PROCESS_TIMEOUT)
|
187
|
-
output = ""
|
188
|
-
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
189
|
-
max_time = start + timeout
|
190
|
-
regexp = /DevTools listening on (ws:\/\/.*)/
|
191
|
-
while (now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)) < max_time
|
192
|
-
begin
|
193
|
-
output += read_io.read_nonblock(512)
|
194
|
-
rescue IO::WaitReadable
|
195
|
-
IO.select([read_io], nil, nil, max_time - now)
|
196
|
-
else
|
197
|
-
if output.match(regexp)
|
198
|
-
@ws_url = Addressable::URI.parse(output.match(regexp)[1].strip)
|
199
|
-
@host = @ws_url.host
|
200
|
-
@port = @ws_url.port
|
201
|
-
break
|
202
|
-
end
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
unless @ws_url
|
207
|
-
@logger.puts output if @logger
|
208
|
-
raise "Chrome process did not produce websocket url within #{timeout} seconds"
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
def close_io(*ios)
|
213
|
-
ios.each do |io|
|
214
|
-
begin
|
215
|
-
io.close unless io.closed?
|
216
|
-
rescue IOError
|
217
|
-
raise unless RUBY_ENGINE == 'jruby'
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
@@ -1,182 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Capybara::Cuprite
|
4
|
-
class Browser
|
5
|
-
module Runtime
|
6
|
-
EXECUTE_OPTIONS = {
|
7
|
-
returnByValue: true,
|
8
|
-
functionDeclaration: %Q(function() { %s })
|
9
|
-
}.freeze
|
10
|
-
DEFAULT_OPTIONS = {
|
11
|
-
functionDeclaration: %Q(function() { return %s })
|
12
|
-
}.freeze
|
13
|
-
EVALUATE_ASYNC_OPTIONS = {
|
14
|
-
awaitPromise: true,
|
15
|
-
functionDeclaration: %Q(
|
16
|
-
function() {
|
17
|
-
return new Promise((__resolve, __reject) => {
|
18
|
-
try {
|
19
|
-
arguments[arguments.length] = r => __resolve(r);
|
20
|
-
arguments.length = arguments.length + 1;
|
21
|
-
setTimeout(() => __reject(new TimedOutPromise), %s);
|
22
|
-
%s
|
23
|
-
} catch(error) {
|
24
|
-
__reject(error);
|
25
|
-
}
|
26
|
-
});
|
27
|
-
}
|
28
|
-
)
|
29
|
-
}.freeze
|
30
|
-
|
31
|
-
def evaluate(expr, *args)
|
32
|
-
response = call(expr, nil, nil, *args)
|
33
|
-
handle(response)
|
34
|
-
end
|
35
|
-
|
36
|
-
def evaluate_in(context_id, expr)
|
37
|
-
response = call(expr, nil, { executionContextId: context_id })
|
38
|
-
handle(response)
|
39
|
-
end
|
40
|
-
|
41
|
-
def evaluate_on(node:, expr:, by_value: true, wait: 0)
|
42
|
-
object_id = command("DOM.resolveNode", nodeId: node["nodeId"]).dig("object", "objectId")
|
43
|
-
options = DEFAULT_OPTIONS.merge(objectId: object_id)
|
44
|
-
options[:functionDeclaration] = options[:functionDeclaration] % expr
|
45
|
-
options.merge!(returnByValue: by_value)
|
46
|
-
|
47
|
-
@wait = wait if wait > 0
|
48
|
-
|
49
|
-
response = command("Runtime.callFunctionOn", **options)
|
50
|
-
.dig("result").tap { |r| handle_error(r) }
|
51
|
-
|
52
|
-
by_value ? response.dig("value") : handle(response)
|
53
|
-
end
|
54
|
-
|
55
|
-
def evaluate_async(expr, wait_time, *args)
|
56
|
-
response = call(expr, wait_time * 1000, EVALUATE_ASYNC_OPTIONS, *args)
|
57
|
-
handle(response)
|
58
|
-
end
|
59
|
-
|
60
|
-
def execute(expr, *args)
|
61
|
-
call(expr, nil, EXECUTE_OPTIONS, *args)
|
62
|
-
true
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
def call(expr, wait_time, options = nil, *args)
|
68
|
-
options ||= {}
|
69
|
-
args = prepare_args(args)
|
70
|
-
|
71
|
-
options = DEFAULT_OPTIONS.merge(options)
|
72
|
-
expr = [wait_time, expr] if wait_time
|
73
|
-
options[:functionDeclaration] = options[:functionDeclaration] % expr
|
74
|
-
options = options.merge(arguments: args)
|
75
|
-
unless options[:executionContextId]
|
76
|
-
options = options.merge(executionContextId: execution_context_id)
|
77
|
-
end
|
78
|
-
|
79
|
-
begin
|
80
|
-
attempts ||= 1
|
81
|
-
response = command("Runtime.callFunctionOn", **options)
|
82
|
-
response.dig("result").tap { |r| handle_error(r) }
|
83
|
-
rescue BrowserError => e
|
84
|
-
case e.message
|
85
|
-
when "No node with given id found", "Could not find node with given id", "Cannot find context with specified id"
|
86
|
-
sleep 0.1
|
87
|
-
attempts += 1
|
88
|
-
options = options.merge(executionContextId: execution_context_id)
|
89
|
-
retry if attempts <= 3
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
# FIXME: We should have a central place to handle all type of errors
|
95
|
-
def handle_error(result)
|
96
|
-
return if result["subtype"] != "error"
|
97
|
-
|
98
|
-
case result["className"]
|
99
|
-
when "TimedOutPromise"
|
100
|
-
raise ScriptTimeoutError
|
101
|
-
when "MouseEventFailed"
|
102
|
-
raise MouseEventFailed.new(result["description"])
|
103
|
-
else
|
104
|
-
raise JavaScriptError.new(result)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def prepare_args(args)
|
109
|
-
args.map do |arg|
|
110
|
-
if arg.is_a?(Node)
|
111
|
-
node_id = arg.native.node["nodeId"]
|
112
|
-
resolved = command("DOM.resolveNode", nodeId: node_id)
|
113
|
-
{ objectId: resolved["object"]["objectId"] }
|
114
|
-
elsif arg.is_a?(Hash) && arg["objectId"]
|
115
|
-
{ objectId: arg["objectId"] }
|
116
|
-
else
|
117
|
-
{ value: arg }
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def handle(response)
|
123
|
-
case response["type"]
|
124
|
-
when "boolean", "number", "string"
|
125
|
-
response["value"]
|
126
|
-
when "undefined"
|
127
|
-
nil
|
128
|
-
when "function"
|
129
|
-
{}
|
130
|
-
when "object"
|
131
|
-
object_id = response["objectId"]
|
132
|
-
|
133
|
-
case response["subtype"]
|
134
|
-
when "node"
|
135
|
-
begin
|
136
|
-
node_id = command("DOM.requestNode", objectId: object_id)["nodeId"]
|
137
|
-
node = command("DOM.describeNode", nodeId: node_id)["node"].merge("nodeId" => node_id)
|
138
|
-
{ "target_id" => target_id, "node" => node }
|
139
|
-
rescue BrowserError => e
|
140
|
-
# Node has disappeared while we were trying to get it
|
141
|
-
raise if e.message != "Could not find node with given id"
|
142
|
-
end
|
143
|
-
when "array"
|
144
|
-
reduce_props(object_id, []) do |memo, key, value|
|
145
|
-
next(memo) unless (Integer(key) rescue nil)
|
146
|
-
value = value["objectId"] ? handle(value) : value["value"]
|
147
|
-
memo.insert(key.to_i, value)
|
148
|
-
end.compact
|
149
|
-
when "date"
|
150
|
-
response["description"]
|
151
|
-
when "null"
|
152
|
-
nil
|
153
|
-
else
|
154
|
-
reduce_props(object_id, {}) do |memo, key, value|
|
155
|
-
value = value["objectId"] ? handle(value) : value["value"]
|
156
|
-
memo.merge(key => value)
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def reduce_props(object_id, to)
|
163
|
-
if cyclic?(object_id).dig("result", "value")
|
164
|
-
return "(cyclic structure)"
|
165
|
-
else
|
166
|
-
props = command("Runtime.getProperties", objectId: object_id)
|
167
|
-
props["result"].reduce(to) do |memo, prop|
|
168
|
-
next(memo) unless prop["enumerable"]
|
169
|
-
yield(memo, prop["name"], prop["value"])
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
def cyclic?(object_id)
|
175
|
-
command("Runtime.callFunctionOn",
|
176
|
-
objectId: object_id,
|
177
|
-
returnByValue: true,
|
178
|
-
functionDeclaration: "function() { return _cuprite.isCyclic(this); }")
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
@@ -1,129 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Capybara::Cuprite
|
4
|
-
class Browser
|
5
|
-
class Targets
|
6
|
-
def initialize(browser)
|
7
|
-
@mutex = Mutex.new
|
8
|
-
@browser = browser
|
9
|
-
@_default = targets.first["targetId"]
|
10
|
-
|
11
|
-
@browser.subscribe("Target.detachedFromTarget") do |params|
|
12
|
-
page = remove_page(params["targetId"])
|
13
|
-
page&.close_connection
|
14
|
-
end
|
15
|
-
|
16
|
-
reset
|
17
|
-
end
|
18
|
-
|
19
|
-
def push(target_id, page = nil)
|
20
|
-
@targets[target_id] = page
|
21
|
-
end
|
22
|
-
|
23
|
-
def refresh
|
24
|
-
@mutex.synchronize do
|
25
|
-
targets.each { |t| push(t["targetId"]) if !default?(t) && !has?(t) }
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def page
|
30
|
-
raise NoSuchWindowError unless @page
|
31
|
-
@page
|
32
|
-
end
|
33
|
-
|
34
|
-
def window_handle
|
35
|
-
page.target_id
|
36
|
-
end
|
37
|
-
|
38
|
-
def window_handles
|
39
|
-
@mutex.synchronize { @targets.keys }
|
40
|
-
end
|
41
|
-
|
42
|
-
def switch_to_window(target_id)
|
43
|
-
@page = find_or_create_page(target_id)
|
44
|
-
end
|
45
|
-
|
46
|
-
def open_new_window
|
47
|
-
target_id = @browser.command("Target.createTarget", url: "about:blank", browserContextId: @_context_id)["targetId"]
|
48
|
-
page = Page.new(target_id, @browser)
|
49
|
-
push(target_id, page)
|
50
|
-
target_id
|
51
|
-
end
|
52
|
-
|
53
|
-
def close_window(target_id)
|
54
|
-
remove_page(target_id)&.close
|
55
|
-
end
|
56
|
-
|
57
|
-
def within_window(locator)
|
58
|
-
original = window_handle
|
59
|
-
|
60
|
-
if Capybara::VERSION.to_f < 3.0
|
61
|
-
target_id = window_handles.find do |target_id|
|
62
|
-
page = find_or_create_page(target_id)
|
63
|
-
locator == page.frame_name
|
64
|
-
end
|
65
|
-
locator = target_id if target_id
|
66
|
-
end
|
67
|
-
|
68
|
-
if window_handles.include?(locator)
|
69
|
-
switch_to_window(locator)
|
70
|
-
yield
|
71
|
-
else
|
72
|
-
raise NoSuchWindowError
|
73
|
-
end
|
74
|
-
ensure
|
75
|
-
switch_to_window(original)
|
76
|
-
end
|
77
|
-
|
78
|
-
def reset
|
79
|
-
if @page
|
80
|
-
@page.close
|
81
|
-
@browser.command("Target.disposeBrowserContext", browserContextId: @_context_id)
|
82
|
-
end
|
83
|
-
|
84
|
-
@page = nil
|
85
|
-
@targets = {}
|
86
|
-
@_context_id = nil
|
87
|
-
|
88
|
-
@_context_id = @browser.command("Target.createBrowserContext")["browserContextId"]
|
89
|
-
target_id = @browser.command("Target.createTarget", url: "about:blank", browserContextId: @_context_id)["targetId"]
|
90
|
-
@page = Page.new(target_id, @browser)
|
91
|
-
push(target_id, @page)
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
def find_or_create_page(target_id)
|
97
|
-
page = @targets[target_id]
|
98
|
-
page ||= Page.new(target_id, @browser)
|
99
|
-
@targets[target_id] ||= page
|
100
|
-
page
|
101
|
-
end
|
102
|
-
|
103
|
-
def remove_page(target_id)
|
104
|
-
page = @targets.delete(target_id)
|
105
|
-
@page = nil if page && @page == page
|
106
|
-
page
|
107
|
-
end
|
108
|
-
|
109
|
-
def targets
|
110
|
-
attempts ||= 1
|
111
|
-
# Targets cannot be empty the must be at least one default target.
|
112
|
-
targets = @browser.command("Target.getTargets")["targetInfos"]
|
113
|
-
raise TypeError if targets.empty?
|
114
|
-
targets
|
115
|
-
rescue TypeError
|
116
|
-
attempts += 1
|
117
|
-
retry if attempts < 3
|
118
|
-
end
|
119
|
-
|
120
|
-
def default?(target)
|
121
|
-
@_default == target["targetId"]
|
122
|
-
end
|
123
|
-
|
124
|
-
def has?(target)
|
125
|
-
@targets.key?(target["targetId"])
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
@@ -1,69 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
require "socket"
|
5
|
-
require "websocket/driver"
|
6
|
-
|
7
|
-
module Capybara::Cuprite
|
8
|
-
class Browser
|
9
|
-
class WebSocket
|
10
|
-
attr_reader :url, :messages
|
11
|
-
|
12
|
-
def initialize(url, logger)
|
13
|
-
@url = url
|
14
|
-
@logger = logger
|
15
|
-
uri = URI.parse(@url)
|
16
|
-
@sock = TCPSocket.new(uri.host, uri.port)
|
17
|
-
@driver = ::WebSocket::Driver.client(self)
|
18
|
-
@messages = Queue.new
|
19
|
-
|
20
|
-
@driver.on(:open, &method(:on_open))
|
21
|
-
@driver.on(:message, &method(:on_message))
|
22
|
-
@driver.on(:close, &method(:on_close))
|
23
|
-
|
24
|
-
@thread = Thread.new do
|
25
|
-
begin
|
26
|
-
while data = @sock.readpartial(512)
|
27
|
-
@driver.parse(data)
|
28
|
-
end
|
29
|
-
rescue EOFError, Errno::ECONNRESET
|
30
|
-
@messages.close
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
@thread.priority = 1
|
35
|
-
|
36
|
-
@driver.start
|
37
|
-
end
|
38
|
-
|
39
|
-
def on_open(_event)
|
40
|
-
sleep 0.01 # https://github.com/faye/websocket-driver-ruby/issues/46
|
41
|
-
end
|
42
|
-
|
43
|
-
def on_message(event)
|
44
|
-
data = JSON.parse(event.data)
|
45
|
-
@messages.push(data)
|
46
|
-
@logger&.puts(" <<< #{event.data}\n")
|
47
|
-
end
|
48
|
-
|
49
|
-
def on_close(_event)
|
50
|
-
@messages.close
|
51
|
-
@thread.kill
|
52
|
-
end
|
53
|
-
|
54
|
-
def send_message(data)
|
55
|
-
json = data.to_json
|
56
|
-
@driver.text(json)
|
57
|
-
@logger&.puts("\n\n>>> #{json}")
|
58
|
-
end
|
59
|
-
|
60
|
-
def write(data)
|
61
|
-
@sock.write(data)
|
62
|
-
end
|
63
|
-
|
64
|
-
def close
|
65
|
-
@driver.close
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
@@ -1,25 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Capybara::Cuprite::Network
|
4
|
-
class Error
|
5
|
-
def initialize(data)
|
6
|
-
@data = data
|
7
|
-
end
|
8
|
-
|
9
|
-
def id
|
10
|
-
@data["networkRequestId"]
|
11
|
-
end
|
12
|
-
|
13
|
-
def url
|
14
|
-
@data["url"]
|
15
|
-
end
|
16
|
-
|
17
|
-
def description
|
18
|
-
@data["text"]
|
19
|
-
end
|
20
|
-
|
21
|
-
def time
|
22
|
-
@time ||= Time.strptime(@data["timestamp"].to_s, "%s")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "time"
|
4
|
-
|
5
|
-
module Capybara::Cuprite::Network
|
6
|
-
class Request
|
7
|
-
attr_accessor :response, :error
|
8
|
-
|
9
|
-
def initialize(data)
|
10
|
-
@data = data
|
11
|
-
end
|
12
|
-
|
13
|
-
def id
|
14
|
-
@data["id"]
|
15
|
-
end
|
16
|
-
|
17
|
-
def url
|
18
|
-
@data["url"]
|
19
|
-
end
|
20
|
-
|
21
|
-
def method
|
22
|
-
@data["method"]
|
23
|
-
end
|
24
|
-
|
25
|
-
def headers
|
26
|
-
@data["headers"]
|
27
|
-
end
|
28
|
-
|
29
|
-
def time
|
30
|
-
@time ||= Time.strptime(@data["time"].to_s, "%s")
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Capybara::Cuprite::Network
|
4
|
-
class Response
|
5
|
-
attr_accessor :body_size
|
6
|
-
|
7
|
-
def initialize(data)
|
8
|
-
@data = data
|
9
|
-
end
|
10
|
-
|
11
|
-
def id
|
12
|
-
@data["id"]
|
13
|
-
end
|
14
|
-
|
15
|
-
def url
|
16
|
-
@data["url"]
|
17
|
-
end
|
18
|
-
|
19
|
-
def status
|
20
|
-
@data["status"]
|
21
|
-
end
|
22
|
-
|
23
|
-
def status_text
|
24
|
-
@data["statusText"]
|
25
|
-
end
|
26
|
-
|
27
|
-
def headers
|
28
|
-
@data["headers"]
|
29
|
-
end
|
30
|
-
|
31
|
-
def headers_size
|
32
|
-
@data["encodedDataLength"]
|
33
|
-
end
|
34
|
-
|
35
|
-
# FIXME: didn't check if we have it on redirect response
|
36
|
-
def redirect_url
|
37
|
-
@data["redirectURL"]
|
38
|
-
end
|
39
|
-
|
40
|
-
def content_type
|
41
|
-
@content_type ||= @data.dig("headers", "contentType").sub(/;.*\z/, "")
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|