capybara-lightpanda 0.1.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 +7 -0
- data/CHANGELOG.md +50 -0
- data/LICENSE.txt +27 -0
- data/NOTICE.md +101 -0
- data/README.md +215 -0
- data/lib/capybara/lightpanda/binary.rb +190 -0
- data/lib/capybara/lightpanda/browser.rb +963 -0
- data/lib/capybara/lightpanda/client/subscriber.rb +44 -0
- data/lib/capybara/lightpanda/client/web_socket.rb +160 -0
- data/lib/capybara/lightpanda/client.rb +124 -0
- data/lib/capybara/lightpanda/cookies.rb +181 -0
- data/lib/capybara/lightpanda/driver.rb +252 -0
- data/lib/capybara/lightpanda/errors.rb +76 -0
- data/lib/capybara/lightpanda/frame.rb +33 -0
- data/lib/capybara/lightpanda/javascripts/index.js +1108 -0
- data/lib/capybara/lightpanda/keyboard.rb +142 -0
- data/lib/capybara/lightpanda/logger.rb +37 -0
- data/lib/capybara/lightpanda/network.rb +92 -0
- data/lib/capybara/lightpanda/node.rb +726 -0
- data/lib/capybara/lightpanda/options.rb +63 -0
- data/lib/capybara/lightpanda/process.rb +252 -0
- data/lib/capybara/lightpanda/utils/event.rb +37 -0
- data/lib/capybara/lightpanda/version.rb +7 -0
- data/lib/capybara/lightpanda/xpath_polyfill.rb +10 -0
- data/lib/capybara-lightpanda.rb +42 -0
- metadata +119 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Lightpanda
|
|
5
|
+
class Keyboard # rubocop:disable Metrics/ClassLength
|
|
6
|
+
# Capybara symbol -> CDP key definition.
|
|
7
|
+
KEYS = {
|
|
8
|
+
cancel: { key: "Cancel", code: "Abort", keyCode: 3 },
|
|
9
|
+
help: { key: "Help", code: "Help", keyCode: 6 },
|
|
10
|
+
backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
11
|
+
tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
12
|
+
clear: { key: "Clear", code: "NumLock", keyCode: 12 },
|
|
13
|
+
return: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
14
|
+
enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
15
|
+
shift: { key: "Shift", code: "ShiftLeft", keyCode: 16 },
|
|
16
|
+
control: { key: "Control", code: "ControlLeft", keyCode: 17 },
|
|
17
|
+
alt: { key: "Alt", code: "AltLeft", keyCode: 18 },
|
|
18
|
+
pause: { key: "Pause", code: "Pause", keyCode: 19 },
|
|
19
|
+
escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
20
|
+
space: { key: " ", code: "Space", keyCode: 32, text: " " },
|
|
21
|
+
page_up: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
22
|
+
page_down: { key: "PageDown", code: "PageDown", keyCode: 34 },
|
|
23
|
+
end: { key: "End", code: "End", keyCode: 35 },
|
|
24
|
+
home: { key: "Home", code: "Home", keyCode: 36 },
|
|
25
|
+
left: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
26
|
+
up: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
27
|
+
right: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
28
|
+
down: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
29
|
+
insert: { key: "Insert", code: "Insert", keyCode: 45 },
|
|
30
|
+
delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
31
|
+
semicolon: { key: ";", code: "Semicolon", keyCode: 186, text: ";" },
|
|
32
|
+
equals: { key: "=", code: "Equal", keyCode: 187, text: "=" },
|
|
33
|
+
numpad0: { key: "0", code: "Numpad0", keyCode: 96, text: "0" },
|
|
34
|
+
numpad1: { key: "1", code: "Numpad1", keyCode: 97, text: "1" },
|
|
35
|
+
numpad2: { key: "2", code: "Numpad2", keyCode: 98, text: "2" },
|
|
36
|
+
numpad3: { key: "3", code: "Numpad3", keyCode: 99, text: "3" },
|
|
37
|
+
numpad4: { key: "4", code: "Numpad4", keyCode: 100, text: "4" },
|
|
38
|
+
numpad5: { key: "5", code: "Numpad5", keyCode: 101, text: "5" },
|
|
39
|
+
numpad6: { key: "6", code: "Numpad6", keyCode: 102, text: "6" },
|
|
40
|
+
numpad7: { key: "7", code: "Numpad7", keyCode: 103, text: "7" },
|
|
41
|
+
numpad8: { key: "8", code: "Numpad8", keyCode: 104, text: "8" },
|
|
42
|
+
numpad9: { key: "9", code: "Numpad9", keyCode: 105, text: "9" },
|
|
43
|
+
multiply: { key: "*", code: "NumpadMultiply", keyCode: 106, text: "*" },
|
|
44
|
+
add: { key: "+", code: "NumpadAdd", keyCode: 107, text: "+" },
|
|
45
|
+
separator: { key: ".", code: "NumpadDecimal", keyCode: 110, text: "." },
|
|
46
|
+
subtract: { key: "-", code: "NumpadSubtract", keyCode: 109, text: "-" },
|
|
47
|
+
decimal: { key: ".", code: "NumpadDecimal", keyCode: 110, text: "." },
|
|
48
|
+
divide: { key: "/", code: "NumpadDivide", keyCode: 111, text: "/" },
|
|
49
|
+
f1: { key: "F1", code: "F1", keyCode: 112 },
|
|
50
|
+
f2: { key: "F2", code: "F2", keyCode: 113 },
|
|
51
|
+
f3: { key: "F3", code: "F3", keyCode: 114 },
|
|
52
|
+
f4: { key: "F4", code: "F4", keyCode: 115 },
|
|
53
|
+
f5: { key: "F5", code: "F5", keyCode: 116 },
|
|
54
|
+
f6: { key: "F6", code: "F6", keyCode: 117 },
|
|
55
|
+
f7: { key: "F7", code: "F7", keyCode: 118 },
|
|
56
|
+
f8: { key: "F8", code: "F8", keyCode: 119 },
|
|
57
|
+
f9: { key: "F9", code: "F9", keyCode: 120 },
|
|
58
|
+
f10: { key: "F10", code: "F10", keyCode: 121 },
|
|
59
|
+
f11: { key: "F11", code: "F11", keyCode: 122 },
|
|
60
|
+
f12: { key: "F12", code: "F12", keyCode: 123 },
|
|
61
|
+
meta: { key: "Meta", code: "MetaLeft", keyCode: 91 },
|
|
62
|
+
command: { key: "Meta", code: "MetaLeft", keyCode: 91 },
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
MODIFIERS = {
|
|
66
|
+
alt: 1,
|
|
67
|
+
control: 2, ctrl: 2,
|
|
68
|
+
meta: 4, command: 4,
|
|
69
|
+
shift: 8,
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
def initialize(browser)
|
|
73
|
+
@browser = browser
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def type(*keys)
|
|
77
|
+
keys.each do |key|
|
|
78
|
+
case key
|
|
79
|
+
when Symbol then dispatch_key(key)
|
|
80
|
+
when String then key.each_char { |char| dispatch_char(char) }
|
|
81
|
+
when Array then type_with_modifiers(key)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def dispatch_key(key)
|
|
89
|
+
definition = KEYS.fetch(key) { raise ArgumentError, "Unknown key: #{key.inspect}" }
|
|
90
|
+
raw_dispatch(definition)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def dispatch_char(char)
|
|
94
|
+
@browser.page_command("Input.insertText", text: char)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def type_with_modifiers(keys)
|
|
98
|
+
modifiers, chars = keys.partition { |k| k.is_a?(Symbol) && MODIFIERS.key?(k) }
|
|
99
|
+
modifier_value = modifiers.sum { |m| MODIFIERS[m] }
|
|
100
|
+
|
|
101
|
+
modifiers.each { |m| send_key_event("rawKeyDown", KEYS[m]) }
|
|
102
|
+
chars.each { |key| dispatch_modified(key, modifier_value, modifiers) }
|
|
103
|
+
modifiers.reverse_each { |m| send_key_event("keyUp", KEYS[m]) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def dispatch_modified(key, modifier_value, modifiers)
|
|
107
|
+
case key
|
|
108
|
+
when Symbol
|
|
109
|
+
definition = KEYS.fetch(key) { raise ArgumentError, "Unknown key: #{key.inspect}" }
|
|
110
|
+
raw_dispatch(definition, modifiers: modifier_value)
|
|
111
|
+
when String
|
|
112
|
+
key.each_char { |char| dispatch_modified_char(char, modifier_value, modifiers) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def dispatch_modified_char(char, modifier_value, modifiers)
|
|
117
|
+
text = modifiers.include?(:shift) ? char.upcase : char
|
|
118
|
+
send_key_event("keyDown", { key: text, text: text, unmodifiedText: char }, modifiers: modifier_value)
|
|
119
|
+
send_key_event("keyUp", { key: text }, modifiers: modifier_value)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def raw_dispatch(definition, modifiers: 0)
|
|
123
|
+
type = definition[:text] ? "keyDown" : "rawKeyDown"
|
|
124
|
+
send_key_event(type, definition, modifiers: modifiers)
|
|
125
|
+
send_key_event("keyUp", definition, modifiers: modifiers)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def send_key_event(type, definition, modifiers: 0)
|
|
129
|
+
params = {
|
|
130
|
+
type: type,
|
|
131
|
+
key: definition[:key],
|
|
132
|
+
code: definition[:code],
|
|
133
|
+
}
|
|
134
|
+
params[:windowsVirtualKeyCode] = definition[:keyCode] if definition[:keyCode]
|
|
135
|
+
params[:text] = definition[:text] if definition[:text]
|
|
136
|
+
params[:unmodifiedText] = definition[:unmodifiedText] || definition[:text] if definition[:text]
|
|
137
|
+
params[:modifiers] = modifiers if modifiers.positive?
|
|
138
|
+
@browser.page_command("Input.dispatchKeyEvent", **params)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Lightpanda
|
|
5
|
+
class Logger
|
|
6
|
+
attr_reader :output
|
|
7
|
+
|
|
8
|
+
def initialize(output = nil)
|
|
9
|
+
@output = output
|
|
10
|
+
@suppressed = false
|
|
11
|
+
@started_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def puts(message)
|
|
15
|
+
return if @suppressed || @output.nil?
|
|
16
|
+
|
|
17
|
+
@output.puts(message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def suppress
|
|
21
|
+
prev = @suppressed
|
|
22
|
+
@suppressed = true
|
|
23
|
+
yield
|
|
24
|
+
ensure
|
|
25
|
+
@suppressed = prev
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def suppressed?
|
|
29
|
+
@suppressed
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def elapsed_time
|
|
33
|
+
format("%.3fs", ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @started_at)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Lightpanda
|
|
5
|
+
class Network
|
|
6
|
+
attr_reader :browser
|
|
7
|
+
|
|
8
|
+
def initialize(browser)
|
|
9
|
+
@browser = browser
|
|
10
|
+
@traffic = []
|
|
11
|
+
@enabled = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def enable
|
|
15
|
+
return if @enabled
|
|
16
|
+
|
|
17
|
+
browser.command("Network.enable")
|
|
18
|
+
subscribe
|
|
19
|
+
@enabled = true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def disable
|
|
23
|
+
return unless @enabled
|
|
24
|
+
|
|
25
|
+
browser.command("Network.disable")
|
|
26
|
+
@enabled = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def traffic
|
|
30
|
+
@traffic.dup
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear
|
|
34
|
+
@traffic.clear
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def headers=(headers)
|
|
38
|
+
@extra_headers = headers
|
|
39
|
+
browser.page_command("Network.setExtraHTTPHeaders", headers: headers)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add_headers(headers)
|
|
43
|
+
@extra_headers = (@extra_headers || {}).merge(headers)
|
|
44
|
+
browser.page_command("Network.setExtraHTTPHeaders", headers: @extra_headers)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def clear_headers
|
|
48
|
+
@extra_headers = {}
|
|
49
|
+
browser.page_command("Network.setExtraHTTPHeaders", headers: {})
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def wait_for_idle(timeout: 5, connections: 0) # rubocop:disable Naming/PredicateMethod
|
|
53
|
+
started_at = Time.now
|
|
54
|
+
|
|
55
|
+
while Time.now - started_at < timeout
|
|
56
|
+
pending = @traffic.count { |t| t[:response].nil? }
|
|
57
|
+
return true if pending <= connections
|
|
58
|
+
|
|
59
|
+
sleep 0.1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def subscribe
|
|
68
|
+
browser.on("Network.requestWillBeSent") do |params|
|
|
69
|
+
@traffic << {
|
|
70
|
+
request_id: params["requestId"],
|
|
71
|
+
url: params.dig("request", "url"),
|
|
72
|
+
method: params.dig("request", "method"),
|
|
73
|
+
timestamp: params["timestamp"],
|
|
74
|
+
response: nil,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
browser.on("Network.responseReceived") do |params|
|
|
79
|
+
request = @traffic.find { |t| t[:request_id] == params["requestId"] }
|
|
80
|
+
|
|
81
|
+
next unless request
|
|
82
|
+
|
|
83
|
+
request[:response] = {
|
|
84
|
+
status: params.dig("response", "status"),
|
|
85
|
+
headers: params.dig("response", "headers"),
|
|
86
|
+
mime_type: params.dig("response", "mimeType"),
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|