cuprite 0.6.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|