capybara-lightpanda 0.9.0 → 0.11.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/CHANGELOG.md +51 -0
- data/README.md +14 -1
- data/lib/capybara/lightpanda/auto_scripts.rb +3 -1
- data/lib/capybara/lightpanda/binary.rb +70 -8
- data/lib/capybara/lightpanda/browser/console.rb +89 -7
- data/lib/capybara/lightpanda/browser/modals.rb +54 -3
- data/lib/capybara/lightpanda/browser/navigation.rb +1 -0
- data/lib/capybara/lightpanda/browser/selenium_compat.rb +124 -0
- data/lib/capybara/lightpanda/browser.rb +104 -4
- data/lib/capybara/lightpanda/downloads.rb +176 -0
- data/lib/capybara/lightpanda/driver.rb +109 -2
- data/lib/capybara/lightpanda/errors.rb +5 -0
- data/lib/capybara/lightpanda/javascripts/errors.js +74 -0
- data/lib/capybara/lightpanda/javascripts/predicates.js +19 -55
- data/lib/capybara/lightpanda/network.rb +36 -10
- data/lib/capybara/lightpanda/node.rb +357 -65
- data/lib/capybara/lightpanda/options.rb +53 -7
- data/lib/capybara/lightpanda/process.rb +173 -11
- data/lib/capybara/lightpanda/version.rb +1 -1
- data/lib/capybara-lightpanda.rb +1 -0
- metadata +5 -2
|
@@ -92,61 +92,25 @@ function isContentEditable(el) {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
DIV:1, DL:1, DT:1, DD:1, FIELDSET:1, FIGCAPTION:1, FIGURE:1,
|
|
104
|
-
FOOTER:1, FORM:1, H1:1, H2:1, H3:1, H4:1, H5:1, H6:1, HEADER:1,
|
|
105
|
-
HGROUP:1, HR:1, LI:1, MAIN:1, NAV:1, OL:1, P:1, PRE:1, SECTION:1,
|
|
106
|
-
TABLE:1, TR:1, UL:1 };
|
|
107
|
-
|
|
108
|
-
// Walk descendants and accumulate text from visible nodes only. Inserts
|
|
109
|
-
// newlines around block-display containers so paragraphs/lists render with
|
|
110
|
-
// natural breaks (Capybara expects this from Chrome's innerText).
|
|
111
|
-
// Lightpanda's innerText returns textContent verbatim (no rendering, so no
|
|
112
|
-
// hidden-descendant filtering), so we DIY the visibility filtering here.
|
|
95
|
+
// Capybara's "visible text" = WebDriver getText semantics, which native
|
|
96
|
+
// innerText alone doesn't provide at two edges: a non-rendered element must
|
|
97
|
+
// yield "" (innerText returns textContent per the HTML getter's step 1), and a
|
|
98
|
+
// ShadowRoot/DocumentFragment has no innerText at all. So we gate on visibility
|
|
99
|
+
// and otherwise delegate the rendered-text collection — block line breaks +
|
|
100
|
+
// display:none descendant skipping + whitespace — to native innerText
|
|
101
|
+
// (Lightpanda #2785/#2795, guaranteed by the build floor). node.rb#visible_text
|
|
102
|
+
// normalizes the whitespace on top.
|
|
113
103
|
function visibleText(el) {
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (node.nodeType === 11) {
|
|
125
|
-
var fout = '';
|
|
126
|
-
for (var k = 0; k < node.childNodes.length; k++) fout += walk(node.childNodes[k]);
|
|
127
|
-
return fout;
|
|
128
|
-
}
|
|
129
|
-
if (node.nodeType !== 1) return '';
|
|
130
|
-
if (!isVisible(node)) return '';
|
|
131
|
-
var tag = (node.tagName || '').toUpperCase();
|
|
132
|
-
if (tag === 'TEXTAREA') return node.value || '';
|
|
133
|
-
if (tag === 'BR') return '\n';
|
|
134
|
-
var win = node.ownerDocument.defaultView || window;
|
|
135
|
-
var style = win.getComputedStyle(node);
|
|
136
|
-
var disp = (style.display || '').toUpperCase();
|
|
137
|
-
var isBlock = BLOCK_DISP[disp] || BLOCK_TAG[tag];
|
|
138
|
-
var out = '';
|
|
139
|
-
for (var i = 0; i < node.childNodes.length; i++) {
|
|
140
|
-
out += walk(node.childNodes[i]);
|
|
141
|
-
}
|
|
142
|
-
// Block-level elements get wrapped in \n…\n only when they actually
|
|
143
|
-
// contribute visible text. An empty <div> between two inline siblings
|
|
144
|
-
// would otherwise introduce a phantom line break that Chrome's
|
|
145
|
-
// innerText algorithm collapses out (required line breaks around
|
|
146
|
-
// empty blocks coalesce in the line-collapse pass).
|
|
147
|
-
if (isBlock && /\S/.test(out)) out = '\n' + out + '\n';
|
|
148
|
-
return out;
|
|
104
|
+
if (el.nodeType === 1) return isVisible(el) ? el.innerText : '';
|
|
105
|
+
// ShadowRoot / DocumentFragment: no innerText of its own. Concatenate the
|
|
106
|
+
// native innerText of each visible element child with the text nodes between
|
|
107
|
+
// them — collapsing the latter's whitespace to a single space so template
|
|
108
|
+
// newlines between inline children render as a separating space, not a break.
|
|
109
|
+
var out = '';
|
|
110
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
111
|
+
var c = el.childNodes[i];
|
|
112
|
+
if (c.nodeType === 3) out += c.nodeValue.replace(/[\t\n\r\f\v ]+/g, ' ');
|
|
113
|
+
else if (c.nodeType === 1 && isVisible(c)) out += c.innerText;
|
|
149
114
|
}
|
|
150
|
-
|
|
151
|
-
return walk(el);
|
|
115
|
+
return out;
|
|
152
116
|
}
|
|
@@ -150,10 +150,11 @@ module Capybara
|
|
|
150
150
|
# The handlers also capture the last top-level navigation response
|
|
151
151
|
# (mirrors capybara-playwright-driver's navigation_request? hook). CDP
|
|
152
152
|
# normally marks the main-document response via
|
|
153
|
-
# `Network.responseReceived.type
|
|
154
|
-
# responses
|
|
155
|
-
# long way: remember the document requestId, store
|
|
156
|
-
# requestId equals it.
|
|
153
|
+
# `Network.responseReceived.type`. Lightpanda only started emitting that
|
|
154
|
+
# field on responses in build 8318 (#3037) — far above the gem's floor —
|
|
155
|
+
# so match the long way instead: remember the document requestId, store
|
|
156
|
+
# the response whose requestId equals it. Works on every supported build;
|
|
157
|
+
# don't "simplify" it to read response.type until the floor clears 8318.
|
|
157
158
|
def subscribe
|
|
158
159
|
@request_handler = build_request_handler
|
|
159
160
|
@response_handler = build_response_handler
|
|
@@ -161,6 +162,16 @@ module Capybara
|
|
|
161
162
|
browser.on("Network.responseReceived", &@response_handler)
|
|
162
163
|
end
|
|
163
164
|
|
|
165
|
+
# Redirects follow Chrome's shape (Lightpanda since #3175, build ≥8602):
|
|
166
|
+
# every followed hop re-emits requestWillBeSent with the SAME requestId
|
|
167
|
+
# and the 3xx riding along as `redirectResponse`; the 3xx never gets a
|
|
168
|
+
# responseReceived of its own. So the hop's event is what closes the
|
|
169
|
+
# previous entry — without it that entry stays `response: nil` forever,
|
|
170
|
+
# pending_connections wedges at ≥1 and every wait_for_idle burns its
|
|
171
|
+
# full timeout after the first redirect (Ferrum's
|
|
172
|
+
# subscribe_request_will_be_sent does the same close). Deliberately NOT
|
|
173
|
+
# fed into @last_navigation_response: status_code must report the final
|
|
174
|
+
# hop, and the redirected requestWillBeSent resets it below anyway.
|
|
164
175
|
def build_request_handler
|
|
165
176
|
lambda do |params|
|
|
166
177
|
if params["type"] == "Document"
|
|
@@ -175,6 +186,9 @@ module Capybara
|
|
|
175
186
|
response: nil,
|
|
176
187
|
}
|
|
177
188
|
@traffic_mutex.synchronize do
|
|
189
|
+
if (redirect = params["redirectResponse"]) && (previous = last_open_entry(params["requestId"]))
|
|
190
|
+
previous[:response] = response_summary(redirect)
|
|
191
|
+
end
|
|
178
192
|
@traffic << entry
|
|
179
193
|
@traffic.shift(@traffic.size - TRAFFIC_LIMIT) if @traffic.size > TRAFFIC_LIMIT
|
|
180
194
|
end
|
|
@@ -190,18 +204,30 @@ module Capybara
|
|
|
190
204
|
}
|
|
191
205
|
end
|
|
192
206
|
@traffic_mutex.synchronize do
|
|
193
|
-
|
|
207
|
+
# Last open entry, not `find`: after a redirect chain several
|
|
208
|
+
# entries share the requestId and only the newest is still open.
|
|
209
|
+
request = last_open_entry(params["requestId"])
|
|
194
210
|
next unless request
|
|
195
211
|
|
|
196
|
-
request[:response] =
|
|
197
|
-
status: params.dig("response", "status"),
|
|
198
|
-
headers: params.dig("response", "headers"),
|
|
199
|
-
mime_type: params.dig("response", "mimeType"),
|
|
200
|
-
}
|
|
212
|
+
request[:response] = response_summary(params["response"])
|
|
201
213
|
end
|
|
202
214
|
end
|
|
203
215
|
end
|
|
204
216
|
|
|
217
|
+
# Caller holds @traffic_mutex.
|
|
218
|
+
def last_open_entry(request_id)
|
|
219
|
+
@traffic.reverse_each.find { |t| t[:request_id] == request_id && t[:response].nil? }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def response_summary(response)
|
|
223
|
+
response ||= {}
|
|
224
|
+
{
|
|
225
|
+
status: response["status"],
|
|
226
|
+
headers: response["headers"],
|
|
227
|
+
mime_type: response["mimeType"],
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
|
|
205
231
|
def unsubscribe
|
|
206
232
|
browser.off("Network.requestWillBeSent", @request_handler) if @request_handler
|
|
207
233
|
browser.off("Network.responseReceived", @response_handler) if @response_handler
|