cuprite 0.2.1 → 0.3.0
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 +15 -7
- data/lib/capybara/cuprite.rb +1 -1
- data/lib/capybara/cuprite/browser.rb +3 -22
- data/lib/capybara/cuprite/browser/client.rb +3 -0
- data/lib/capybara/cuprite/browser/dom.rb +2 -2
- data/lib/capybara/cuprite/browser/frame.rb +5 -1
- data/lib/capybara/cuprite/browser/input.json +1341 -0
- data/lib/capybara/cuprite/browser/input.rb +79 -10
- data/lib/capybara/cuprite/browser/javascripts/index.js +14 -1
- data/lib/capybara/cuprite/browser/page.rb +59 -1
- data/lib/capybara/cuprite/browser/process.rb +36 -23
- data/lib/capybara/cuprite/browser/runtime.rb +20 -48
- data/lib/capybara/cuprite/browser/targets.rb +17 -4
- data/lib/capybara/cuprite/browser/web_socket.rb +20 -11
- data/lib/capybara/cuprite/driver.rb +5 -27
- data/lib/capybara/cuprite/errors.rb +0 -6
- data/lib/capybara/cuprite/version.rb +1 -1
- metadata +28 -27
@@ -1,6 +1,27 @@
|
|
1
1
|
module Capybara::Cuprite
|
2
2
|
class Browser
|
3
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
|
+
|
4
25
|
def click(node, keys = [], offset = {})
|
5
26
|
x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
|
6
27
|
command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 1)
|
@@ -59,21 +80,66 @@ module Capybara::Cuprite
|
|
59
80
|
end
|
60
81
|
|
61
82
|
def send_keys(node, keys)
|
83
|
+
keys = normalize_keys(Array(keys))
|
84
|
+
|
62
85
|
click(node) if !evaluate_on(node: node, expr: %(_cuprite.containsSelection(this)))
|
63
86
|
|
64
|
-
keys.
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
70
107
|
else
|
71
|
-
|
72
|
-
|
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
|
73
132
|
end
|
74
133
|
end
|
75
134
|
end
|
76
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
|
+
|
77
143
|
private
|
78
144
|
|
79
145
|
def prepare_before_click(name, node, keys, offset)
|
@@ -81,8 +147,7 @@ module Capybara::Cuprite
|
|
81
147
|
x, y = calculate_quads(node, offset[:x], offset[:y])
|
82
148
|
evaluate_on(node: node, expr: "_cuprite.mouseEventTest(this, '#{name}', #{x}, #{y})")
|
83
149
|
|
84
|
-
|
85
|
-
modifiers = keys.map { |k| click_modifiers[k.to_sym] }.compact.reduce(0, :|)
|
150
|
+
modifiers = keys.map { |k| MODIFIERS[k.to_s] }.compact.reduce(0, :|)
|
86
151
|
|
87
152
|
command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y)
|
88
153
|
|
@@ -117,6 +182,10 @@ module Capybara::Cuprite
|
|
117
182
|
{x: quad[6], y: quad[7]}]
|
118
183
|
end.first
|
119
184
|
end
|
185
|
+
|
186
|
+
def to_options(hash)
|
187
|
+
hash.inject({}) { |memo, (k, v)| memo.merge(k.to_sym => v) }
|
188
|
+
end
|
120
189
|
end
|
121
190
|
end
|
122
191
|
end
|
@@ -10,6 +10,10 @@ const EVENTS = {
|
|
10
10
|
}
|
11
11
|
|
12
12
|
class Cuprite {
|
13
|
+
constructor() {
|
14
|
+
this._json = JSON; // In case someone overrides it like mootools
|
15
|
+
}
|
16
|
+
|
13
17
|
find(method, selector, within = document) {
|
14
18
|
try {
|
15
19
|
let results = [];
|
@@ -386,7 +390,7 @@ class Cuprite {
|
|
386
390
|
attrs[attr.name] = attr.value.replace("\n", "\\n");
|
387
391
|
}
|
388
392
|
|
389
|
-
return
|
393
|
+
return this._json.stringify(attrs);
|
390
394
|
}
|
391
395
|
|
392
396
|
getAttribute(node, name) {
|
@@ -435,6 +439,15 @@ class Cuprite {
|
|
435
439
|
|
436
440
|
return node.contains(selectedNode);
|
437
441
|
}
|
442
|
+
|
443
|
+
isCyclic(object) {
|
444
|
+
try {
|
445
|
+
this._json.stringify(object);
|
446
|
+
return false;
|
447
|
+
} catch (e) {
|
448
|
+
return true;
|
449
|
+
}
|
450
|
+
}
|
438
451
|
}
|
439
452
|
|
440
453
|
window._cuprite = new Cuprite;
|
@@ -43,6 +43,8 @@ module Capybara::Cuprite
|
|
43
43
|
@frames = {}
|
44
44
|
@waiting_frames ||= Set.new
|
45
45
|
@frame_stack = []
|
46
|
+
@accept_modal = []
|
47
|
+
@modal_messages = []
|
46
48
|
|
47
49
|
begin
|
48
50
|
@session_id = @browser.command("Target.attachToTarget", targetId: @target_id)["sessionId"]
|
@@ -72,7 +74,7 @@ module Capybara::Cuprite
|
|
72
74
|
options = { url: url }
|
73
75
|
options.merge!(referrer: referrer) if referrer
|
74
76
|
response = command("Page.navigate", **options)
|
75
|
-
if response["errorText"]
|
77
|
+
if %w[net::ERR_NAME_NOT_RESOLVED net::ERR_NAME_RESOLUTION_FAILED].include?(response["errorText"])
|
76
78
|
raise StatusFailError, "url" => url
|
77
79
|
end
|
78
80
|
response["frameId"]
|
@@ -123,6 +125,49 @@ module Capybara::Cuprite
|
|
123
125
|
go(1)
|
124
126
|
end
|
125
127
|
|
128
|
+
def accept_confirm
|
129
|
+
@accept_modal << true
|
130
|
+
end
|
131
|
+
|
132
|
+
def dismiss_confirm
|
133
|
+
@accept_modal << false
|
134
|
+
end
|
135
|
+
|
136
|
+
def accept_prompt(modal_response)
|
137
|
+
@accept_modal << true
|
138
|
+
@modal_response = modal_response
|
139
|
+
end
|
140
|
+
|
141
|
+
def dismiss_prompt
|
142
|
+
@accept_modal << false
|
143
|
+
end
|
144
|
+
|
145
|
+
def find_modal(options)
|
146
|
+
start_time = Time.now
|
147
|
+
timeout_sec = options.fetch(:wait) { session_wait_time }
|
148
|
+
expect_text = options[:text]
|
149
|
+
expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s)
|
150
|
+
not_found_msg = "Unable to find modal dialog"
|
151
|
+
not_found_msg += " with #{expect_text}" if expect_text
|
152
|
+
|
153
|
+
begin
|
154
|
+
modal_text = @modal_messages.shift
|
155
|
+
raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
|
156
|
+
rescue Capybara::ModalNotFound => e
|
157
|
+
raise e, not_found_msg if (Time.now - start_time) >= timeout_sec
|
158
|
+
sleep(0.05)
|
159
|
+
retry
|
160
|
+
end
|
161
|
+
|
162
|
+
modal_text
|
163
|
+
end
|
164
|
+
|
165
|
+
def reset_modals
|
166
|
+
@accept_modal = []
|
167
|
+
@modal_response = nil
|
168
|
+
@modal_messages = []
|
169
|
+
end
|
170
|
+
|
126
171
|
def command(*args)
|
127
172
|
id = nil
|
128
173
|
|
@@ -151,6 +196,19 @@ module Capybara::Cuprite
|
|
151
196
|
end
|
152
197
|
end
|
153
198
|
|
199
|
+
@client.subscribe("Page.javascriptDialogOpening") do |params|
|
200
|
+
accept_modal = @accept_modal.last
|
201
|
+
if accept_modal == true || accept_modal == false
|
202
|
+
@accept_modal.pop
|
203
|
+
@modal_messages << params["message"]
|
204
|
+
options = { accept: accept_modal }
|
205
|
+
default = params["defaultPrompt"]
|
206
|
+
response = @modal_response || params["defaultPrompt"]
|
207
|
+
options.merge!(promptText: response) if response
|
208
|
+
@client.command("Page.handleJavaScriptDialog", **options)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
154
212
|
@client.subscribe("Page.windowOpen") do
|
155
213
|
@browser.targets.refresh
|
156
214
|
@mutex.try_lock
|
@@ -7,7 +7,7 @@ module Capybara::Cuprite
|
|
7
7
|
class Process
|
8
8
|
KILL_TIMEOUT = 2
|
9
9
|
|
10
|
-
BROWSER_PATH = ENV
|
10
|
+
BROWSER_PATH = ENV["BROWSER_PATH"]
|
11
11
|
BROWSER_HOST = "127.0.0.1"
|
12
12
|
BROWSER_PORT = "0"
|
13
13
|
|
@@ -25,7 +25,7 @@ module Capybara::Cuprite
|
|
25
25
|
"disable-web-security" => nil,
|
26
26
|
}.freeze
|
27
27
|
|
28
|
-
attr_reader :host, :port, :ws_url, :pid, :options
|
28
|
+
attr_reader :host, :port, :ws_url, :pid, :path, :options
|
29
29
|
|
30
30
|
def self.start(*args)
|
31
31
|
new(*args).tap(&:start)
|
@@ -54,15 +54,8 @@ module Capybara::Cuprite
|
|
54
54
|
|
55
55
|
def initialize(options)
|
56
56
|
@options = options.fetch(:browser, {})
|
57
|
-
exe = options[:path] || BROWSER_PATH
|
58
|
-
@path = Cliver.detect(exe)
|
59
57
|
|
60
|
-
|
61
|
-
message = "Could not find an executable `#{exe}`. Try to make it " \
|
62
|
-
"available on the PATH or set environment varible for " \
|
63
|
-
"example BROWSER_PATH=\"/Applications/Chromium.app/Contents/MacOS/Chromium\""
|
64
|
-
raise Cliver::Dependency::NotFound.new(message)
|
65
|
-
end
|
58
|
+
detect_browser_path
|
66
59
|
|
67
60
|
window_size = options.fetch(:window_size, [1024, 768])
|
68
61
|
@options = @options.merge("window-size" => window_size.join(","))
|
@@ -106,6 +99,24 @@ module Capybara::Cuprite
|
|
106
99
|
start
|
107
100
|
end
|
108
101
|
|
102
|
+
def detect_browser_path
|
103
|
+
exe = @options[:path] || BROWSER_PATH
|
104
|
+
if RUBY_PLATFORM.include?('darwin')
|
105
|
+
exe ||= "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
106
|
+
@path = exe if File.exist?(exe)
|
107
|
+
else
|
108
|
+
exe ||= "chrome"
|
109
|
+
@path = Cliver.detect(exe) || Cliver.detect("google-chrome")
|
110
|
+
end
|
111
|
+
|
112
|
+
unless @path
|
113
|
+
message = "Could not find an executable `#{exe}`. Try to make it " \
|
114
|
+
"available on the PATH or set environment varible for " \
|
115
|
+
"example BROWSER_PATH=\"/Applications/Chromium.app/Contents/MacOS/Chromium\""
|
116
|
+
raise Cliver::Dependency::NotFound.new(message)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
109
120
|
private
|
110
121
|
|
111
122
|
def redirect_stdout(write_io)
|
@@ -130,26 +141,28 @@ module Capybara::Cuprite
|
|
130
141
|
@pid = nil
|
131
142
|
end
|
132
143
|
|
133
|
-
def parse_ws_url(read_io)
|
144
|
+
def parse_ws_url(read_io, timeout = 1)
|
134
145
|
output = ""
|
135
|
-
|
146
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
147
|
+
max_time = start + timeout
|
136
148
|
regexp = /DevTools listening on (ws:\/\/.*)/
|
137
|
-
|
149
|
+
while (now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)) < max_time
|
138
150
|
begin
|
139
151
|
output += read_io.read_nonblock(512)
|
140
152
|
rescue IO::WaitReadable
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
153
|
+
IO.select([read_io], nil, nil, max_time - now)
|
154
|
+
else
|
155
|
+
if output.match(regexp)
|
156
|
+
@ws_url = Addressable::URI.parse(output.match(regexp)[1])
|
157
|
+
@host = @ws_url.host
|
158
|
+
@port = @ws_url.port
|
159
|
+
break
|
160
|
+
end
|
145
161
|
end
|
162
|
+
end
|
146
163
|
|
147
|
-
|
148
|
-
|
149
|
-
@host = @ws_url.host
|
150
|
-
@port = @ws_url.port
|
151
|
-
break
|
152
|
-
end
|
164
|
+
unless @ws_url
|
165
|
+
raise "Chrome process did not produce websocket url within #{timeout} seconds"
|
153
166
|
end
|
154
167
|
end
|
155
168
|
|
@@ -108,7 +108,7 @@ module Capybara::Cuprite
|
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
|
-
def handle(response
|
111
|
+
def handle(response)
|
112
112
|
case response["type"]
|
113
113
|
when "boolean", "number", "string"
|
114
114
|
response["value"]
|
@@ -117,15 +117,17 @@ module Capybara::Cuprite
|
|
117
117
|
when "function"
|
118
118
|
{}
|
119
119
|
when "object"
|
120
|
+
object_id = response["objectId"]
|
121
|
+
|
120
122
|
case response["subtype"]
|
121
123
|
when "node"
|
122
|
-
node_id = command("DOM.requestNode", objectId:
|
123
|
-
node = command("DOM.describeNode", nodeId: node_id)["node"]
|
124
|
-
{ "target_id" => target_id, "node" => node
|
124
|
+
node_id = command("DOM.requestNode", objectId: object_id)["nodeId"]
|
125
|
+
node = command("DOM.describeNode", nodeId: node_id)["node"].merge("nodeId" => node_id)
|
126
|
+
{ "target_id" => target_id, "node" => node }
|
125
127
|
when "array"
|
126
|
-
|
128
|
+
reduce_props(object_id, []) do |memo, key, value|
|
127
129
|
next(memo) unless (Integer(key) rescue nil)
|
128
|
-
value = value["objectId"] ? handle(value
|
130
|
+
value = value["objectId"] ? handle(value) : value["value"]
|
129
131
|
memo.insert(key.to_i, value)
|
130
132
|
end
|
131
133
|
when "date"
|
@@ -133,61 +135,31 @@ module Capybara::Cuprite
|
|
133
135
|
when "null"
|
134
136
|
nil
|
135
137
|
else
|
136
|
-
|
137
|
-
value = value["objectId"] ? handle(value
|
138
|
+
reduce_props(object_id, {}) do |memo, key, value|
|
139
|
+
value = value["objectId"] ? handle(value) : value["value"]
|
138
140
|
memo.merge(key => value)
|
139
141
|
end
|
140
142
|
end
|
141
143
|
end
|
142
|
-
ensure
|
143
|
-
clean if cleanup
|
144
144
|
end
|
145
145
|
|
146
|
-
def
|
147
|
-
if
|
148
|
-
"(cyclic structure)"
|
146
|
+
def reduce_props(object_id, to)
|
147
|
+
if cyclic?(object_id).dig("result", "value")
|
148
|
+
return "(cyclic structure)"
|
149
149
|
else
|
150
|
-
|
150
|
+
props = command("Runtime.getProperties", objectId: object_id)
|
151
|
+
props["result"].reduce(to) do |memo, prop|
|
151
152
|
next(memo) unless prop["enumerable"]
|
152
153
|
yield(memo, prop["name"], prop["value"])
|
153
154
|
end
|
154
155
|
end
|
155
156
|
end
|
156
157
|
|
157
|
-
def
|
158
|
-
command("Runtime.
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
# id each time {"objectId":"{\"injectedScriptId\":1,\"id\":1}"} and it's
|
163
|
-
# impossible to check that two objects are actually equal. This workaround
|
164
|
-
# does equality check only in JS runtime. `_cuprite` can be inavailable here
|
165
|
-
# if page is about:blank for example.
|
166
|
-
def visited?(object_id)
|
167
|
-
expr = %Q(
|
168
|
-
let object = arguments[0];
|
169
|
-
let callback = arguments[1];
|
170
|
-
|
171
|
-
if (window._cupriteVisitedObjects === undefined) {
|
172
|
-
window._cupriteVisitedObjects = [];
|
173
|
-
}
|
174
|
-
|
175
|
-
let visited = window._cupriteVisitedObjects;
|
176
|
-
if (visited.some(o => o === object)) {
|
177
|
-
callback(true);
|
178
|
-
} else {
|
179
|
-
visited.push(object);
|
180
|
-
callback(false);
|
181
|
-
}
|
182
|
-
)
|
183
|
-
|
184
|
-
# FIXME: Is there a way we can use wait_time here?
|
185
|
-
response = call(expr, 5, EVALUATE_ASYNC_OPTIONS, { "objectId" => object_id })
|
186
|
-
handle(response, false)
|
187
|
-
end
|
188
|
-
|
189
|
-
def clean
|
190
|
-
execute("delete window._cupriteVisitedObjects")
|
158
|
+
def cyclic?(object_id)
|
159
|
+
command("Runtime.callFunctionOn",
|
160
|
+
objectId: object_id,
|
161
|
+
returnByValue: true,
|
162
|
+
functionDeclaration: "function() { return _cuprite.isCyclic(this); }")
|
191
163
|
end
|
192
164
|
end
|
193
165
|
end
|