puppeteer-bidi 0.0.1.beta10 → 0.0.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/AGENTS.md +44 -0
- data/API_COVERAGE.md +345 -0
- data/CLAUDE/porting_puppeteer.md +20 -0
- data/CLAUDE.md +2 -1
- data/DEVELOPMENT.md +14 -0
- data/README.md +47 -415
- data/development/generate_api_coverage.rb +411 -0
- data/development/puppeteer_revision.txt +1 -0
- data/lib/puppeteer/bidi/browser.rb +118 -22
- data/lib/puppeteer/bidi/browser_context.rb +185 -2
- data/lib/puppeteer/bidi/connection.rb +16 -5
- data/lib/puppeteer/bidi/cookie_utils.rb +192 -0
- data/lib/puppeteer/bidi/core/browsing_context.rb +83 -40
- data/lib/puppeteer/bidi/core/realm.rb +6 -0
- data/lib/puppeteer/bidi/core/request.rb +79 -35
- data/lib/puppeteer/bidi/core/user_context.rb +5 -3
- data/lib/puppeteer/bidi/element_handle.rb +200 -8
- data/lib/puppeteer/bidi/errors.rb +4 -0
- data/lib/puppeteer/bidi/frame.rb +115 -11
- data/lib/puppeteer/bidi/http_request.rb +577 -0
- data/lib/puppeteer/bidi/http_response.rb +161 -10
- data/lib/puppeteer/bidi/locator.rb +792 -0
- data/lib/puppeteer/bidi/page.rb +859 -7
- data/lib/puppeteer/bidi/query_handler.rb +1 -1
- data/lib/puppeteer/bidi/version.rb +1 -1
- data/lib/puppeteer/bidi.rb +39 -6
- data/sig/puppeteer/bidi/browser.rbs +53 -6
- data/sig/puppeteer/bidi/browser_context.rbs +36 -0
- data/sig/puppeteer/bidi/cookie_utils.rbs +64 -0
- data/sig/puppeteer/bidi/core/browsing_context.rbs +16 -6
- data/sig/puppeteer/bidi/core/request.rbs +14 -11
- data/sig/puppeteer/bidi/core/user_context.rbs +2 -2
- data/sig/puppeteer/bidi/element_handle.rbs +28 -0
- data/sig/puppeteer/bidi/errors.rbs +4 -0
- data/sig/puppeteer/bidi/frame.rbs +17 -0
- data/sig/puppeteer/bidi/http_request.rbs +162 -0
- data/sig/puppeteer/bidi/http_response.rbs +67 -8
- data/sig/puppeteer/bidi/locator.rbs +267 -0
- data/sig/puppeteer/bidi/page.rbs +170 -0
- data/sig/puppeteer/bidi.rbs +15 -3
- metadata +12 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
ROOT = Pathname.new(__dir__).join("..").expand_path
|
|
9
|
+
CACHE_SCHEMA_VERSION = 1
|
|
10
|
+
|
|
11
|
+
def run_capture(*command, chdir: nil)
|
|
12
|
+
output = if chdir
|
|
13
|
+
IO.popen(command, chdir: chdir.to_s, err: %i[child out], &:read)
|
|
14
|
+
else
|
|
15
|
+
IO.popen(command, err: %i[child out], &:read)
|
|
16
|
+
end
|
|
17
|
+
status = $?
|
|
18
|
+
raise "Command failed: #{command.join(" ")}\n#{output}" unless status&.success?
|
|
19
|
+
|
|
20
|
+
output
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def read_puppeteer_commit(puppeteer_dir)
|
|
24
|
+
run_capture("git", "rev-parse", "HEAD", chdir: puppeteer_dir.to_s).strip
|
|
25
|
+
rescue StandardError
|
|
26
|
+
"unknown"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find_puppeteer_doc_paths(puppeteer_dir)
|
|
30
|
+
candidates = []
|
|
31
|
+
|
|
32
|
+
api_md = puppeteer_dir.join("docs/api.md")
|
|
33
|
+
candidates << api_md if api_md.file?
|
|
34
|
+
|
|
35
|
+
api_dir = puppeteer_dir.join("docs/api")
|
|
36
|
+
if api_dir.directory?
|
|
37
|
+
Dir.glob(api_dir.join("**/*.md")).sort.each do |path|
|
|
38
|
+
candidates << Pathname.new(path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
candidates.uniq
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extract_frontmatter_value(markdown, key)
|
|
46
|
+
in_frontmatter = false
|
|
47
|
+
markdown.each_line do |line|
|
|
48
|
+
stripped = line.strip
|
|
49
|
+
if stripped == "---"
|
|
50
|
+
in_frontmatter = !in_frontmatter
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
next unless in_frontmatter
|
|
54
|
+
|
|
55
|
+
next unless stripped.start_with?("#{key}:")
|
|
56
|
+
|
|
57
|
+
value = stripped.delete_prefix("#{key}:").strip
|
|
58
|
+
value = value[1..-2] if (value.start_with?('"') && value.end_with?('"')) || (value.start_with?("'") && value.end_with?("'"))
|
|
59
|
+
return value.strip
|
|
60
|
+
end
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extract_heading_api_references(markdown, source:)
|
|
65
|
+
refs = []
|
|
66
|
+
|
|
67
|
+
markdown.each_line do |line|
|
|
68
|
+
next unless line.start_with?("#")
|
|
69
|
+
|
|
70
|
+
heading = line.sub(/\A#+\s+/, "").strip
|
|
71
|
+
next if heading.empty?
|
|
72
|
+
|
|
73
|
+
token = heading.split(/[\s(:—-]/, 2).first.to_s.strip
|
|
74
|
+
next if token.empty?
|
|
75
|
+
next if token.start_with?("event:", "type:", "class:", "interface:")
|
|
76
|
+
|
|
77
|
+
token = token.delete_suffix("method")
|
|
78
|
+
token = token.delete_suffix("property")
|
|
79
|
+
token = token.delete_suffix("class")
|
|
80
|
+
token = token.delete_suffix("()")
|
|
81
|
+
token = token.sub(/\Anew\s+/, "")
|
|
82
|
+
token = token[1..-2] if token.start_with?("`") && token.end_with?("`") && token.length >= 2
|
|
83
|
+
|
|
84
|
+
owner = nil
|
|
85
|
+
member = nil
|
|
86
|
+
|
|
87
|
+
if token.include?(".")
|
|
88
|
+
owner, member = token.split(".", 2)
|
|
89
|
+
elsif token.include?("#")
|
|
90
|
+
owner, member = token.split("#", 2)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
next if owner.nil? || member.nil?
|
|
94
|
+
next if owner.empty? || member.empty?
|
|
95
|
+
|
|
96
|
+
refs << { "owner" => owner, "member" => member, "source" => source }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
refs
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def extract_puppeteer_api(puppeteer_dir)
|
|
103
|
+
doc_paths = find_puppeteer_doc_paths(puppeteer_dir)
|
|
104
|
+
raise "Could not find Puppeteer docs under #{puppeteer_dir}" if doc_paths.empty?
|
|
105
|
+
|
|
106
|
+
entries = []
|
|
107
|
+
doc_paths.each do |path|
|
|
108
|
+
markdown = path.read
|
|
109
|
+
rel = path.relative_path_from(puppeteer_dir).to_s
|
|
110
|
+
|
|
111
|
+
sidebar_label = extract_frontmatter_value(markdown, "sidebar_label")
|
|
112
|
+
if sidebar_label && (sidebar_label.include?(".") || sidebar_label.include?("#"))
|
|
113
|
+
token = sidebar_label.strip
|
|
114
|
+
token = token[1..-2] if token.start_with?("`") && token.end_with?("`") && token.length >= 2
|
|
115
|
+
owner = nil
|
|
116
|
+
member = nil
|
|
117
|
+
if token.include?(".")
|
|
118
|
+
owner, member = token.split(".", 2)
|
|
119
|
+
elsif token.include?("#")
|
|
120
|
+
owner, member = token.split("#", 2)
|
|
121
|
+
end
|
|
122
|
+
if owner && member && !owner.empty? && !member.empty?
|
|
123
|
+
entries << { "owner" => owner, "member" => member, "source" => rel }
|
|
124
|
+
next
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
entries.concat(extract_heading_api_references(markdown, source: rel))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
dedup = {}
|
|
132
|
+
entries.each do |e|
|
|
133
|
+
key = "#{e["owner"]}.#{e["member"]}"
|
|
134
|
+
dedup[key] ||= e
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
dedup.values.sort_by { |e| [e["owner"].downcase, e["member"].downcase] }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def camel_to_snake(name)
|
|
141
|
+
name
|
|
142
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
143
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
144
|
+
.tr("-", "_")
|
|
145
|
+
.downcase
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
SPECIAL_MEMBER_MAPPINGS = {
|
|
149
|
+
"$" => "query_selector",
|
|
150
|
+
"$$" => "query_selector_all",
|
|
151
|
+
"$eval" => "eval_on_selector",
|
|
152
|
+
"$$eval" => "eval_on_selector_all",
|
|
153
|
+
"waitForSelector" => "wait_for_selector",
|
|
154
|
+
"waitForXPath" => "wait_for_xpath",
|
|
155
|
+
"waitForFunction" => "wait_for_function",
|
|
156
|
+
"waitForNavigation" => "wait_for_navigation",
|
|
157
|
+
"waitForRequest" => "wait_for_request",
|
|
158
|
+
"waitForResponse" => "wait_for_response",
|
|
159
|
+
"waitForFileChooser" => "wait_for_file_chooser",
|
|
160
|
+
"evaluateHandle" => "evaluate_handle",
|
|
161
|
+
"evaluateOnNewDocument" => "evaluate_on_new_document",
|
|
162
|
+
"setContent" => "set_content",
|
|
163
|
+
"setViewport" => "set_viewport",
|
|
164
|
+
"setUserAgent" => "set_user_agent",
|
|
165
|
+
"setExtraHTTPHeaders" => "set_extra_http_headers",
|
|
166
|
+
"isClosed" => "closed?"
|
|
167
|
+
}.freeze
|
|
168
|
+
|
|
169
|
+
def ruby_member_candidates(node_member)
|
|
170
|
+
mapped = SPECIAL_MEMBER_MAPPINGS[node_member]
|
|
171
|
+
return [mapped] if mapped
|
|
172
|
+
|
|
173
|
+
snake = camel_to_snake(node_member)
|
|
174
|
+
candidates = [snake]
|
|
175
|
+
|
|
176
|
+
if snake.start_with?("is_") && !snake.end_with?("?")
|
|
177
|
+
candidates << "#{snake.delete_prefix("is_")}?"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
candidates.uniq
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
NODE_OWNER_ALIASES = {
|
|
184
|
+
"puppeteer" => "Puppeteer",
|
|
185
|
+
"Puppeteer" => "Puppeteer",
|
|
186
|
+
"PuppeteerNode" => "Puppeteer",
|
|
187
|
+
"browser" => "Browser",
|
|
188
|
+
"Browser" => "Browser",
|
|
189
|
+
"browsercontext" => "BrowserContext",
|
|
190
|
+
"browserContext" => "BrowserContext",
|
|
191
|
+
"BrowserContext" => "BrowserContext",
|
|
192
|
+
"page" => "Page",
|
|
193
|
+
"Page" => "Page",
|
|
194
|
+
"frame" => "Frame",
|
|
195
|
+
"Frame" => "Frame",
|
|
196
|
+
"elementhandle" => "ElementHandle",
|
|
197
|
+
"elementHandle" => "ElementHandle",
|
|
198
|
+
"ElementHandle" => "ElementHandle",
|
|
199
|
+
"jshandle" => "JSHandle",
|
|
200
|
+
"jsHandle" => "JSHandle",
|
|
201
|
+
"JSHandle" => "JSHandle",
|
|
202
|
+
"keyboard" => "Keyboard",
|
|
203
|
+
"Keyboard" => "Keyboard",
|
|
204
|
+
"mouse" => "Mouse",
|
|
205
|
+
"Mouse" => "Mouse",
|
|
206
|
+
"httprequest" => "HTTPRequest",
|
|
207
|
+
"httpRequest" => "HTTPRequest",
|
|
208
|
+
"HTTPRequest" => "HTTPRequest",
|
|
209
|
+
"httpresponse" => "HTTPResponse",
|
|
210
|
+
"httpResponse" => "HTTPResponse",
|
|
211
|
+
"HTTPResponse" => "HTTPResponse",
|
|
212
|
+
"filechooser" => "FileChooser",
|
|
213
|
+
"fileChooser" => "FileChooser",
|
|
214
|
+
"FileChooser" => "FileChooser",
|
|
215
|
+
"target" => "Target",
|
|
216
|
+
"Target" => "Target"
|
|
217
|
+
}.freeze
|
|
218
|
+
|
|
219
|
+
def canonical_node_owner(owner)
|
|
220
|
+
NODE_OWNER_ALIASES[owner] || NODE_OWNER_ALIASES[owner.downcase] || owner
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
RUBY_OWNER_CONSTANTS = {
|
|
224
|
+
"Puppeteer" => "Puppeteer::Bidi",
|
|
225
|
+
"Browser" => "Puppeteer::Bidi::Browser",
|
|
226
|
+
"BrowserContext" => "Puppeteer::Bidi::BrowserContext",
|
|
227
|
+
"Page" => "Puppeteer::Bidi::Page",
|
|
228
|
+
"Frame" => "Puppeteer::Bidi::Frame",
|
|
229
|
+
"ElementHandle" => "Puppeteer::Bidi::ElementHandle",
|
|
230
|
+
"JSHandle" => "Puppeteer::Bidi::JSHandle",
|
|
231
|
+
"Keyboard" => "Puppeteer::Bidi::Keyboard",
|
|
232
|
+
"Mouse" => "Puppeteer::Bidi::Mouse",
|
|
233
|
+
"HTTPRequest" => "Puppeteer::Bidi::HTTPRequest",
|
|
234
|
+
"HTTPResponse" => "Puppeteer::Bidi::HTTPResponse",
|
|
235
|
+
"FileChooser" => "Puppeteer::Bidi::FileChooser",
|
|
236
|
+
"Target" => "Puppeteer::Bidi::Target"
|
|
237
|
+
}.freeze
|
|
238
|
+
|
|
239
|
+
def safe_constantize(name)
|
|
240
|
+
name.split("::").inject(Object) { |mod, const_name| mod.const_get(const_name) }
|
|
241
|
+
rescue NameError
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def extract_ruby_public_api
|
|
246
|
+
$LOAD_PATH.unshift(ROOT.join("lib").to_s)
|
|
247
|
+
require "puppeteer/bidi"
|
|
248
|
+
|
|
249
|
+
api = {}
|
|
250
|
+
lib_root = ROOT.join("lib").to_s
|
|
251
|
+
|
|
252
|
+
RUBY_OWNER_CONSTANTS.each do |label, const_name|
|
|
253
|
+
constant = safe_constantize(const_name)
|
|
254
|
+
next unless constant
|
|
255
|
+
|
|
256
|
+
if constant.is_a?(Module) && !constant.is_a?(Class)
|
|
257
|
+
methods = constant.singleton_methods(true).map(&:to_s)
|
|
258
|
+
methods.select! do |method_name|
|
|
259
|
+
method_obj = constant.method(method_name)
|
|
260
|
+
owner_name = method_obj.owner.name
|
|
261
|
+
owner_name&.start_with?("Puppeteer::Bidi") ||
|
|
262
|
+
(method_obj.source_location && method_obj.source_location.first.start_with?(lib_root))
|
|
263
|
+
rescue NameError
|
|
264
|
+
false
|
|
265
|
+
end
|
|
266
|
+
api[label] = { kind: :module, const_name: const_name, methods: methods.sort.uniq }
|
|
267
|
+
next
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
methods = constant.public_instance_methods(true).map(&:to_s)
|
|
271
|
+
methods.select! do |method_name|
|
|
272
|
+
owner_name = constant.instance_method(method_name).owner.name
|
|
273
|
+
owner_name&.start_with?("Puppeteer::Bidi")
|
|
274
|
+
rescue NameError
|
|
275
|
+
false
|
|
276
|
+
end
|
|
277
|
+
api[label] = { kind: :class, const_name: const_name, methods: methods.sort.uniq }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
api
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def load_cache(cache_file, expected_commit:)
|
|
284
|
+
return nil unless cache_file.file?
|
|
285
|
+
|
|
286
|
+
data = JSON.parse(cache_file.read)
|
|
287
|
+
return nil unless data.is_a?(Hash)
|
|
288
|
+
return nil unless data["schema_version"] == CACHE_SCHEMA_VERSION
|
|
289
|
+
return nil unless data["puppeteer_commit"] == expected_commit
|
|
290
|
+
|
|
291
|
+
entries = data["entries"]
|
|
292
|
+
return nil unless entries.is_a?(Array)
|
|
293
|
+
|
|
294
|
+
entries
|
|
295
|
+
rescue JSON::ParserError
|
|
296
|
+
nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def write_cache(cache_file, puppeteer_commit:, entries:)
|
|
300
|
+
cache_file.parent.mkpath
|
|
301
|
+
payload = {
|
|
302
|
+
"schema_version" => CACHE_SCHEMA_VERSION,
|
|
303
|
+
"puppeteer_commit" => puppeteer_commit,
|
|
304
|
+
"generated_at" => Time.now.utc.iso8601,
|
|
305
|
+
"entries" => entries
|
|
306
|
+
}
|
|
307
|
+
cache_file.write(JSON.pretty_generate(payload) + "\n")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def generate_markdown(puppeteer_commit:, entries:, ruby_api:)
|
|
311
|
+
supported_owners = RUBY_OWNER_CONSTANTS.keys.to_h { |k| [k, true] }
|
|
312
|
+
filtered = entries.select { |e| supported_owners[canonical_node_owner(e["owner"])] }
|
|
313
|
+
grouped = filtered.group_by { |e| canonical_node_owner(e["owner"]) }
|
|
314
|
+
|
|
315
|
+
total = 0
|
|
316
|
+
supported = 0
|
|
317
|
+
|
|
318
|
+
sections = []
|
|
319
|
+
|
|
320
|
+
grouped.keys.sort_by(&:downcase).each do |node_owner_label|
|
|
321
|
+
group = grouped.fetch(node_owner_label)
|
|
322
|
+
ruby_owner_const = RUBY_OWNER_CONSTANTS[node_owner_label]
|
|
323
|
+
ruby_owner = ruby_owner_const ? ruby_api[node_owner_label] : nil
|
|
324
|
+
|
|
325
|
+
heading = ruby_owner_const ? "#{node_owner_label} (#{ruby_owner_const})" : node_owner_label
|
|
326
|
+
section_lines = []
|
|
327
|
+
section_lines << "## #{heading}"
|
|
328
|
+
section_lines << ""
|
|
329
|
+
section_lines << "| Node.js | Ruby | Supported |"
|
|
330
|
+
section_lines << "| --- | --- | :---: |"
|
|
331
|
+
|
|
332
|
+
ruby_methods = ruby_owner ? ruby_owner.fetch(:methods) : []
|
|
333
|
+
ruby_kind = ruby_owner ? ruby_owner.fetch(:kind) : nil
|
|
334
|
+
|
|
335
|
+
group.sort_by { |e| e["member"].downcase }.each do |entry|
|
|
336
|
+
node_owner = entry.fetch("owner")
|
|
337
|
+
node_member = entry.fetch("member")
|
|
338
|
+
node_ref = "#{node_owner}.#{node_member}"
|
|
339
|
+
|
|
340
|
+
ruby_candidates = ruby_member_candidates(node_member)
|
|
341
|
+
ruby_supported_method = ruby_candidates.find { |m| ruby_methods.include?(m) }
|
|
342
|
+
|
|
343
|
+
total += 1
|
|
344
|
+
if ruby_supported_method
|
|
345
|
+
supported += 1
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
ruby_ref = "-"
|
|
349
|
+
if ruby_owner_const && ruby_candidates.any?
|
|
350
|
+
separator = ruby_kind == :module ? "." : "#"
|
|
351
|
+
ruby_method_name = ruby_supported_method || ruby_candidates.first
|
|
352
|
+
ruby_ref = "#{ruby_owner_const}#{separator}#{ruby_method_name}"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
status = ruby_supported_method ? "✅" : "❌"
|
|
356
|
+
section_lines << "| `#{node_ref}` | `#{ruby_ref}` | #{status} |"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
section_lines << ""
|
|
360
|
+
sections << section_lines.join("\n")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
coverage = total.zero? ? 0.0 : (supported.to_f / total) * 100.0
|
|
364
|
+
lines = []
|
|
365
|
+
lines << "# API Coverage"
|
|
366
|
+
lines << ""
|
|
367
|
+
lines << "- Puppeteer commit: `#{puppeteer_commit}`"
|
|
368
|
+
lines << "- Generated by: `development/generate_api_coverage.rb`"
|
|
369
|
+
lines << "- Coverage: `#{supported}/#{total}` (`#{format("%.2f", coverage)}%`)"
|
|
370
|
+
lines << ""
|
|
371
|
+
lines << sections.join("\n")
|
|
372
|
+
|
|
373
|
+
lines.join("\n") + "\n"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
options = {
|
|
377
|
+
puppeteer_dir: ROOT.join("development", "puppeteer").to_s,
|
|
378
|
+
cache_dir: ROOT.join("development", "cache").to_s,
|
|
379
|
+
output: ROOT.join("API_COVERAGE.md").to_s
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
OptionParser.new do |opts|
|
|
383
|
+
opts.banner = "Usage: bundle exec ruby development/generate_api_coverage.rb [options]"
|
|
384
|
+
opts.on("--puppeteer-dir=PATH", "Path to a checked out puppeteer/puppeteer repo") { |v| options[:puppeteer_dir] = v }
|
|
385
|
+
opts.on("--cache-dir=PATH", "Cache directory (default: development/cache)") { |v| options[:cache_dir] = v }
|
|
386
|
+
opts.on("--output=PATH", "Output path (default: API_COVERAGE.md)") { |v| options[:output] = v }
|
|
387
|
+
end.parse!
|
|
388
|
+
|
|
389
|
+
puppeteer_dir = Pathname.new(options.fetch(:puppeteer_dir)).expand_path
|
|
390
|
+
cache_dir = Pathname.new(options.fetch(:cache_dir)).expand_path
|
|
391
|
+
output_path = Pathname.new(options.fetch(:output)).expand_path
|
|
392
|
+
|
|
393
|
+
raise "Puppeteer repo not found: #{puppeteer_dir}" unless puppeteer_dir.directory?
|
|
394
|
+
|
|
395
|
+
puppeteer_commit = read_puppeteer_commit(puppeteer_dir)
|
|
396
|
+
cache_file = cache_dir.join("puppeteer_api.json")
|
|
397
|
+
|
|
398
|
+
entries = load_cache(cache_file, expected_commit: puppeteer_commit)
|
|
399
|
+
unless entries
|
|
400
|
+
entries = extract_puppeteer_api(puppeteer_dir)
|
|
401
|
+
write_cache(cache_file, puppeteer_commit: puppeteer_commit, entries: entries)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
ruby_api = extract_ruby_public_api
|
|
405
|
+
output_path.write(
|
|
406
|
+
generate_markdown(
|
|
407
|
+
puppeteer_commit: puppeteer_commit,
|
|
408
|
+
entries: entries,
|
|
409
|
+
ruby_api: ruby_api
|
|
410
|
+
)
|
|
411
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
7d750c25cb29764f2fb31cb90b750a8eec350199
|
|
@@ -11,32 +11,26 @@ module Puppeteer
|
|
|
11
11
|
attr_reader :connection #: Connection
|
|
12
12
|
attr_reader :process #: untyped
|
|
13
13
|
attr_reader :default_browser_context #: BrowserContext
|
|
14
|
+
attr_reader :ws_endpoint #: String?
|
|
14
15
|
|
|
15
16
|
# @rbs connection: Connection -- BiDi connection
|
|
16
17
|
# @rbs launcher: BrowserLauncher? -- Browser launcher instance
|
|
18
|
+
# @rbs ws_endpoint: String? -- WebSocket endpoint URL
|
|
19
|
+
# @rbs accept_insecure_certs: bool -- Accept insecure certificates
|
|
17
20
|
# @rbs return: Browser -- Browser instance
|
|
18
|
-
def self.create(connection:, launcher: nil)
|
|
21
|
+
def self.create(connection:, launcher: nil, ws_endpoint: nil, accept_insecure_certs: false)
|
|
19
22
|
# Create a new BiDi session
|
|
20
23
|
session = Core::Session.from(
|
|
21
24
|
connection: connection,
|
|
22
25
|
capabilities: {
|
|
23
26
|
alwaysMatch: {
|
|
24
|
-
acceptInsecureCerts:
|
|
27
|
+
acceptInsecureCerts: accept_insecure_certs,
|
|
28
|
+
unhandledPromptBehavior: { default: 'ignore' },
|
|
25
29
|
webSocketUrl: true,
|
|
26
30
|
},
|
|
27
31
|
},
|
|
28
32
|
).wait
|
|
29
33
|
|
|
30
|
-
# Subscribe to BiDi modules before creating browser
|
|
31
|
-
subscribe_modules = %w[
|
|
32
|
-
browsingContext
|
|
33
|
-
network
|
|
34
|
-
log
|
|
35
|
-
script
|
|
36
|
-
input
|
|
37
|
-
]
|
|
38
|
-
session.subscribe(subscribe_modules).wait
|
|
39
|
-
|
|
40
34
|
core_browser = Core::Browser.from(session).wait
|
|
41
35
|
session.browser = core_browser
|
|
42
36
|
|
|
@@ -45,6 +39,7 @@ module Puppeteer
|
|
|
45
39
|
launcher: launcher,
|
|
46
40
|
core_browser: core_browser,
|
|
47
41
|
session: session,
|
|
42
|
+
ws_endpoint: ws_endpoint,
|
|
48
43
|
)
|
|
49
44
|
end
|
|
50
45
|
|
|
@@ -52,13 +47,16 @@ module Puppeteer
|
|
|
52
47
|
# @rbs launcher: BrowserLauncher? -- Browser launcher instance
|
|
53
48
|
# @rbs core_browser: Core::Browser -- Core browser instance
|
|
54
49
|
# @rbs session: Core::Session -- BiDi session
|
|
50
|
+
# @rbs ws_endpoint: String? -- WebSocket endpoint URL
|
|
55
51
|
# @rbs return: void
|
|
56
|
-
def initialize(connection:, launcher:, core_browser:, session:)
|
|
52
|
+
def initialize(connection:, launcher:, core_browser:, session:, ws_endpoint:)
|
|
57
53
|
@connection = connection
|
|
58
54
|
@launcher = launcher
|
|
59
55
|
@closed = false
|
|
56
|
+
@disconnected = false
|
|
60
57
|
@core_browser = core_browser
|
|
61
58
|
@session = session
|
|
59
|
+
@ws_endpoint = ws_endpoint
|
|
62
60
|
|
|
63
61
|
# Create default browser context
|
|
64
62
|
default_user_context = @core_browser.default_user_context
|
|
@@ -74,8 +72,10 @@ module Puppeteer
|
|
|
74
72
|
# @rbs headless: bool -- Run browser in headless mode
|
|
75
73
|
# @rbs args: Array[String]? -- Additional browser arguments
|
|
76
74
|
# @rbs timeout: Numeric? -- Launch timeout in seconds
|
|
75
|
+
# @rbs accept_insecure_certs: bool -- Accept insecure certificates
|
|
77
76
|
# @rbs return: Browser -- Browser instance
|
|
78
|
-
def self.launch(executable_path: nil, user_data_dir: nil, headless: true, args: nil, timeout: nil
|
|
77
|
+
def self.launch(executable_path: nil, user_data_dir: nil, headless: true, args: nil, timeout: nil,
|
|
78
|
+
accept_insecure_certs: false)
|
|
79
79
|
launcher = BrowserLauncher.new(
|
|
80
80
|
executable_path: executable_path,
|
|
81
81
|
user_data_dir: user_data_dir,
|
|
@@ -87,6 +87,7 @@ module Puppeteer
|
|
|
87
87
|
|
|
88
88
|
# Create transport and connection
|
|
89
89
|
transport = Transport.new(ws_endpoint)
|
|
90
|
+
ws_endpoint = transport.url
|
|
90
91
|
|
|
91
92
|
# Start transport connection in background thread with Sync reactor
|
|
92
93
|
# Sync is the preferred way to run async code at the top level
|
|
@@ -94,19 +95,32 @@ module Puppeteer
|
|
|
94
95
|
|
|
95
96
|
connection = Connection.new(transport)
|
|
96
97
|
|
|
97
|
-
browser = create(connection: connection, launcher: launcher
|
|
98
|
-
|
|
98
|
+
browser = create(connection: connection, launcher: launcher, ws_endpoint: ws_endpoint,
|
|
99
|
+
accept_insecure_certs: accept_insecure_certs)
|
|
100
|
+
_target = browser.wait_for_target { |target| target.type == 'page' }
|
|
99
101
|
browser
|
|
100
102
|
end
|
|
101
103
|
|
|
102
104
|
# Connect to an existing Firefox browser instance
|
|
103
105
|
# @rbs ws_endpoint: String -- WebSocket endpoint URL
|
|
106
|
+
# @rbs timeout: Numeric? -- Connect timeout in seconds
|
|
107
|
+
# @rbs accept_insecure_certs: bool -- Accept insecure certificates
|
|
104
108
|
# @rbs return: Browser -- Browser instance
|
|
105
|
-
def self.connect(ws_endpoint)
|
|
109
|
+
def self.connect(ws_endpoint, timeout: nil, accept_insecure_certs: false)
|
|
106
110
|
transport = Transport.new(ws_endpoint)
|
|
107
|
-
|
|
111
|
+
ws_endpoint = transport.url
|
|
112
|
+
timeout_ms = ((timeout || 30) * 1000).to_i
|
|
113
|
+
AsyncUtils.async_timeout(timeout_ms, transport.connect).wait
|
|
108
114
|
connection = Connection.new(transport)
|
|
109
|
-
|
|
115
|
+
|
|
116
|
+
# Verify that this endpoint speaks WebDriver BiDi (and is ready) before creating a new session.
|
|
117
|
+
status = connection.async_send_command('session.status', {}, timeout: timeout_ms).wait
|
|
118
|
+
unless status.is_a?(Hash) && status['ready'] == true
|
|
119
|
+
raise Error, "WebDriver BiDi endpoint is not ready: #{status.inspect}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
create(connection: connection, launcher: nil, ws_endpoint: ws_endpoint,
|
|
123
|
+
accept_insecure_certs: accept_insecure_certs)
|
|
110
124
|
end
|
|
111
125
|
|
|
112
126
|
# Get BiDi session status
|
|
@@ -115,18 +129,63 @@ module Puppeteer
|
|
|
115
129
|
@connection.async_send_command('session.status').wait
|
|
116
130
|
end
|
|
117
131
|
|
|
132
|
+
# Get the browser's original user agent
|
|
133
|
+
# @rbs return: String -- User agent string
|
|
134
|
+
def user_agent
|
|
135
|
+
@session.capabilities["userAgent"]
|
|
136
|
+
end
|
|
137
|
+
|
|
118
138
|
# Create a new page (Puppeteer-like API)
|
|
119
139
|
# @rbs return: Page -- New page instance
|
|
120
140
|
def new_page
|
|
121
141
|
@default_browser_context.new_page
|
|
122
142
|
end
|
|
123
143
|
|
|
144
|
+
# Create a new browser context
|
|
145
|
+
# @rbs return: BrowserContext -- New browser context
|
|
146
|
+
def create_browser_context
|
|
147
|
+
user_context = @core_browser.create_user_context.wait
|
|
148
|
+
browser_context_for(user_context)
|
|
149
|
+
end
|
|
150
|
+
|
|
124
151
|
# Get all pages
|
|
125
152
|
# @rbs return: Array[Page] -- All pages
|
|
126
153
|
def pages
|
|
154
|
+
return [] if @closed || @disconnected
|
|
155
|
+
|
|
127
156
|
@default_browser_context.pages
|
|
128
157
|
end
|
|
129
158
|
|
|
159
|
+
# Get all cookies in the default browser context.
|
|
160
|
+
# @rbs return: Array[Hash[String, untyped]] -- Cookies
|
|
161
|
+
def cookies
|
|
162
|
+
@default_browser_context.cookies
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Set cookies in the default browser context.
|
|
166
|
+
# @rbs *cookies: Array[Hash[String, untyped]] -- Cookie data
|
|
167
|
+
# @rbs **cookie: untyped -- Single cookie via keyword arguments
|
|
168
|
+
# @rbs return: void
|
|
169
|
+
def set_cookie(*cookies, **cookie)
|
|
170
|
+
@default_browser_context.set_cookie(*cookies, **cookie)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Delete cookies in the default browser context.
|
|
174
|
+
# @rbs *cookies: Array[Hash[String, untyped]] -- Cookies to delete
|
|
175
|
+
# @rbs **cookie: untyped -- Single cookie via keyword arguments
|
|
176
|
+
# @rbs return: void
|
|
177
|
+
def delete_cookie(*cookies, **cookie)
|
|
178
|
+
@default_browser_context.delete_cookie(*cookies, **cookie)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Delete cookies matching the provided filters in the default browser context.
|
|
182
|
+
# @rbs *filters: Array[Hash[String, untyped]] -- Cookie filters
|
|
183
|
+
# @rbs **filter: untyped -- Single filter via keyword arguments
|
|
184
|
+
# @rbs return: void
|
|
185
|
+
def delete_matching_cookies(*filters, **filter)
|
|
186
|
+
@default_browser_context.delete_matching_cookies(*filters, **filter)
|
|
187
|
+
end
|
|
188
|
+
|
|
130
189
|
# Register event handler
|
|
131
190
|
# @rbs event: String | Symbol -- Event name
|
|
132
191
|
# @rbs &block: (untyped) -> void -- Event handler
|
|
@@ -143,19 +202,50 @@ module Puppeteer
|
|
|
143
202
|
@closed = true
|
|
144
203
|
|
|
145
204
|
begin
|
|
146
|
-
|
|
205
|
+
begin
|
|
206
|
+
@connection.async_send_command('browser.close', {}).wait
|
|
207
|
+
rescue StandardError => e
|
|
208
|
+
debug_error(e)
|
|
209
|
+
ensure
|
|
210
|
+
@connection.close
|
|
211
|
+
end
|
|
147
212
|
rescue => e
|
|
148
|
-
|
|
213
|
+
debug_error(e)
|
|
149
214
|
end
|
|
150
215
|
|
|
151
216
|
@launcher&.kill
|
|
152
217
|
end
|
|
153
218
|
|
|
219
|
+
# Disconnect from the browser (does not close the browser process).
|
|
220
|
+
# @rbs return: void
|
|
221
|
+
def disconnect
|
|
222
|
+
return if @closed || @disconnected
|
|
223
|
+
|
|
224
|
+
@disconnected = true
|
|
225
|
+
|
|
226
|
+
begin
|
|
227
|
+
@session.end_session
|
|
228
|
+
rescue StandardError => e
|
|
229
|
+
debug_error(e)
|
|
230
|
+
ensure
|
|
231
|
+
begin
|
|
232
|
+
@connection.close
|
|
233
|
+
rescue StandardError => e
|
|
234
|
+
debug_error(e)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
154
239
|
# @rbs return: bool
|
|
155
240
|
def closed?
|
|
156
241
|
@closed
|
|
157
242
|
end
|
|
158
243
|
|
|
244
|
+
# @rbs return: bool
|
|
245
|
+
def disconnected?
|
|
246
|
+
@disconnected
|
|
247
|
+
end
|
|
248
|
+
|
|
159
249
|
# Wait until a target (top-level browsing context) satisfies the predicate.
|
|
160
250
|
# @rbs timeout: Integer? -- Timeout in milliseconds (default: 30000)
|
|
161
251
|
# @rbs &predicate: (BrowserTarget | PageTarget | FrameTarget) -> boolish -- Predicate evaluated against each Target
|
|
@@ -251,6 +341,12 @@ module Puppeteer
|
|
|
251
341
|
|
|
252
342
|
private
|
|
253
343
|
|
|
344
|
+
def debug_error(error)
|
|
345
|
+
return unless ENV['DEBUG_BIDI_COMMAND']
|
|
346
|
+
|
|
347
|
+
warn(error.full_message)
|
|
348
|
+
end
|
|
349
|
+
|
|
254
350
|
# @rbs &block: (BrowserTarget | PageTarget | FrameTarget) -> void -- Block to yield each target to
|
|
255
351
|
# @rbs return: Enumerator[BrowserTarget | PageTarget | FrameTarget, void] -- Enumerator of targets
|
|
256
352
|
def each_target(&block)
|
|
@@ -284,7 +380,7 @@ module Puppeteer
|
|
|
284
380
|
end
|
|
285
381
|
|
|
286
382
|
# @rbs user_context: Core::UserContext -- User context to get browser context for
|
|
287
|
-
# @rbs return: BrowserContext
|
|
383
|
+
# @rbs return: BrowserContext -- Browser context
|
|
288
384
|
def browser_context_for(user_context)
|
|
289
385
|
return @browser_contexts[user_context.id] if @browser_contexts.key?(user_context.id)
|
|
290
386
|
|