puppeteer-bidi 0.0.3.beta2 → 0.0.3.beta3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72e6358ce92df4d8ecc3da2fd620b05e05bf0a2d72140dc9f2922e586ae2a356
4
- data.tar.gz: 7c461d352c323edda5ee2c8ff83004c9ddf8be5fe2f3cb56fb00178a6d9cd3e1
3
+ metadata.gz: eb435eba199162f3270ff09d0efeaf728935717f0c690a0549f176f1be06e072
4
+ data.tar.gz: 7a39e4c65471997c4d37f4558459121a9370769dbf341b7f45b6d2441175b60e
5
5
  SHA512:
6
- metadata.gz: 4e5096f1f616ffa1f5f0a06b390a9fa79cab0a4bf5ffd38f36ee2f2af8de4ee3e8c75b6270cb6fec239164f3363ccd2c2c71bb7ccf81962201324dd4cf8c46e8
7
- data.tar.gz: eb1fd0a20857db922cea98ec9bd8740baed85d57414ae2358adeb6d1d9dd0894f298f77ce8b8339405e52368cc7079fb44428a78f982e54bc3564a39659b0469
6
+ metadata.gz: 1d9b44c59dda746bae25de8ad9e37c9576867fd5f39b434e39e0ea2cb1ce30d05e943496d787e9cc254d5747af1ce8514818dce67c5778861345617788c2b6af
7
+ data.tar.gz: 5d80cc021677292638eb0103cf7aca8f9c87309fcb858425360b1ac6f937f2d5d1e5386c5b30e738589fc787ab7f609a271af18d682ac9452b95ae5fec25d765
@@ -116,7 +116,18 @@ module Puppeteer
116
116
  raise Error, "ReactorRunner is closed"
117
117
  end
118
118
 
119
- promise.wait
119
+ # Ruby 4.0: Async::Promise#wait can wake spuriously; retry until resolved.
120
+ loop do
121
+ begin
122
+ return promise.wait
123
+ rescue TypeError => e
124
+ raise unless e.message == "exception class/object expected"
125
+ next unless promise.resolved?
126
+ value = promise.value
127
+ raise value if value.is_a?(Exception)
128
+ return value
129
+ end
130
+ end
120
131
  end
121
132
 
122
133
  # @rbs return: void
@@ -108,8 +108,11 @@ module Puppeteer
108
108
  warn "Failed to parse BiDi message: #{e.message}"
109
109
  end
110
110
  end
111
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
112
+ # Connection closed - this is expected during shutdown, no need to warn
111
113
  rescue => e
112
- warn "Transport receive error: #{e.message}"
114
+ # Only warn for unexpected errors if we weren't intentionally closed
115
+ warn "Transport receive error: #{e.message}" unless @closed
113
116
  ensure
114
117
  close unless @closed
115
118
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Puppeteer
4
4
  module Bidi
5
- VERSION = "0.0.3.beta2"
5
+ VERSION = "0.0.3.beta3"
6
6
  end
7
7
  end
data/sig/_external.rbs CHANGED
@@ -33,6 +33,7 @@ module Async
33
33
  def resolve: (T) -> void
34
34
  def reject: (Exception) -> void
35
35
  def resolved?: () -> bool
36
+ def value: () -> T?
36
37
  end
37
38
 
38
39
  class TimeoutError < StandardError
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppeteer-bidi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3.beta2
4
+ version: 0.0.3.beta3
5
5
  platform: ruby
6
6
  authors:
7
7
  - YusukeIwaki
@@ -77,15 +77,12 @@ extra_rdoc_files: []
77
77
  files:
78
78
  - ".rspec"
79
79
  - ".rubocop.yml"
80
- - AGENTS.md
81
80
  - API_COVERAGE.md
82
81
  - DEVELOPMENT.md
83
82
  - LICENSE.txt
84
83
  - README.md
85
84
  - Rakefile
86
85
  - Steepfile
87
- - development/generate_api_coverage.rb
88
- - development/puppeteer_revision.txt
89
86
  - lib/puppeteer/bidi.rb
90
87
  - lib/puppeteer/bidi/async_utils.rb
91
88
  - lib/puppeteer/bidi/browser.rb
data/AGENTS.md DELETED
@@ -1,44 +0,0 @@
1
- # Repository Guidelines
2
-
3
- ## Start Here (Project-Specific Guidance)
4
-
5
- - Read `CLAUDE.md` and `CLAUDE/` first; they define the core async architecture, porting workflow, and testing strategy.
6
- - Ruby: requires `>= 3.2` (CI covers 3.2–3.4). Prefer a version manager (`rbenv`, `asdf`, etc.) over system Ruby.
7
-
8
- ## Project Structure & Module Organization
9
-
10
- - `lib/puppeteer/bidi/`: user-facing API (sync, calls `.wait`).
11
- - `lib/puppeteer/bidi/core/`: low-level BiDi core (async, returns `Async::Task`).
12
- - `spec/integration/`: browser-driven specs; fixtures in `spec/assets/`.
13
-
14
- ## Build, Test, and Development Commands
15
-
16
- - See `DEVELOPMENT.md` for the full command list and environment variables.
17
- - Run RSpec via `rbenv exec bundle exec rspec ...` when using rbenv.
18
-
19
- ## Coding Style & Naming Conventions
20
-
21
- - Ruby: 2-space indent, double-quoted strings, ~120 char lines; follow RuboCop (`.rubocop.yml`).
22
- - BiDi-only: do not introduce CDP-related ports.
23
- - Async: core returns `Async::Task`; upper layer must call `.wait` on every core call (see `CLAUDE/two_layer_architecture.md`).
24
- - Reviews to watch: WS messages can be handled out-of-order; “wait for event” code must not hang (register listeners before commands, handle “already happened”, cancel on errors).
25
-
26
- ## Testing Guidelines
27
-
28
- - Prefer `spec/integration/` and the shared-browser `with_test_state` pattern (see `CLAUDE/testing_strategy.md`).
29
- - Use `pending` for browser limitations vs `skip` for unimplemented features.
30
-
31
- ## Commit & Pull Request Guidelines
32
-
33
- - See `DEVELOPMENT.md` for commit, PR, and release conventions.
34
-
35
- ## Agent Notes (Porting/Review)
36
-
37
- - When porting from upstream TS, mirror optional vs default fields: defaults are for validation, and optional keys should be omitted from payloads unless explicitly provided.
38
- - Match upstream error messages as closely as possible (including interpolated values) so tests align with Puppeteer.
39
- - In core layer option checks, use key presence (`options.key?`) when upstream uses `'in'` to distinguish "missing" from `nil`.
40
- - During reviews, compare both implementation (`packages/puppeteer-core/src/bidi/*.ts`) and tests (`test/src/page.spec.ts`) to catch behavior parity gaps.
41
-
42
- ## Security & Configuration Tips
43
-
44
- - Do not commit credentials; review network-fetched changes (e.g., `scripts/update_injected_source.rb` → `lib/puppeteer/bidi/injected.js`) carefully.
@@ -1,412 +0,0 @@
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
- # Note: Target is excluded from coverage tracking due to significant
216
- # implementation differences between Node.js and Ruby versions.
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
- # Note: Target is excluded from coverage tracking due to significant
237
- # implementation differences between Node.js and Ruby versions.
238
- }.freeze
239
-
240
- def safe_constantize(name)
241
- name.split("::").inject(Object) { |mod, const_name| mod.const_get(const_name) }
242
- rescue NameError
243
- nil
244
- end
245
-
246
- def extract_ruby_public_api
247
- $LOAD_PATH.unshift(ROOT.join("lib").to_s)
248
- require "puppeteer/bidi"
249
-
250
- api = {}
251
- lib_root = ROOT.join("lib").to_s
252
-
253
- RUBY_OWNER_CONSTANTS.each do |label, const_name|
254
- constant = safe_constantize(const_name)
255
- next unless constant
256
-
257
- if constant.is_a?(Module) && !constant.is_a?(Class)
258
- methods = constant.singleton_methods(true).map(&:to_s)
259
- methods.select! do |method_name|
260
- method_obj = constant.method(method_name)
261
- owner_name = method_obj.owner.name
262
- owner_name&.start_with?("Puppeteer::Bidi") ||
263
- (method_obj.source_location && method_obj.source_location.first.start_with?(lib_root))
264
- rescue NameError
265
- false
266
- end
267
- api[label] = { kind: :module, const_name: const_name, methods: methods.sort.uniq }
268
- next
269
- end
270
-
271
- methods = constant.public_instance_methods(true).map(&:to_s)
272
- methods.select! do |method_name|
273
- owner_name = constant.instance_method(method_name).owner.name
274
- owner_name&.start_with?("Puppeteer::Bidi")
275
- rescue NameError
276
- false
277
- end
278
- api[label] = { kind: :class, const_name: const_name, methods: methods.sort.uniq }
279
- end
280
-
281
- api
282
- end
283
-
284
- def load_cache(cache_file, expected_commit:)
285
- return nil unless cache_file.file?
286
-
287
- data = JSON.parse(cache_file.read)
288
- return nil unless data.is_a?(Hash)
289
- return nil unless data["schema_version"] == CACHE_SCHEMA_VERSION
290
- return nil unless data["puppeteer_commit"] == expected_commit
291
-
292
- entries = data["entries"]
293
- return nil unless entries.is_a?(Array)
294
-
295
- entries
296
- rescue JSON::ParserError
297
- nil
298
- end
299
-
300
- def write_cache(cache_file, puppeteer_commit:, entries:)
301
- cache_file.parent.mkpath
302
- payload = {
303
- "schema_version" => CACHE_SCHEMA_VERSION,
304
- "puppeteer_commit" => puppeteer_commit,
305
- "generated_at" => Time.now.utc.iso8601,
306
- "entries" => entries
307
- }
308
- cache_file.write(JSON.pretty_generate(payload) + "\n")
309
- end
310
-
311
- def generate_markdown(puppeteer_commit:, entries:, ruby_api:)
312
- supported_owners = RUBY_OWNER_CONSTANTS.keys.to_h { |k| [k, true] }
313
- filtered = entries.select { |e| supported_owners[canonical_node_owner(e["owner"])] }
314
- grouped = filtered.group_by { |e| canonical_node_owner(e["owner"]) }
315
-
316
- total = 0
317
- supported = 0
318
-
319
- sections = []
320
-
321
- grouped.keys.sort_by(&:downcase).each do |node_owner_label|
322
- group = grouped.fetch(node_owner_label)
323
- ruby_owner_const = RUBY_OWNER_CONSTANTS[node_owner_label]
324
- ruby_owner = ruby_owner_const ? ruby_api[node_owner_label] : nil
325
-
326
- heading = ruby_owner_const ? "#{node_owner_label} (#{ruby_owner_const})" : node_owner_label
327
- section_lines = []
328
- section_lines << "## #{heading}"
329
- section_lines << ""
330
- section_lines << "| Node.js | Ruby | Supported |"
331
- section_lines << "| --- | --- | :---: |"
332
-
333
- ruby_methods = ruby_owner ? ruby_owner.fetch(:methods) : []
334
- ruby_kind = ruby_owner ? ruby_owner.fetch(:kind) : nil
335
-
336
- group.sort_by { |e| e["member"].downcase }.each do |entry|
337
- node_owner = entry.fetch("owner")
338
- node_member = entry.fetch("member")
339
- node_ref = "#{node_owner}.#{node_member}"
340
-
341
- ruby_candidates = ruby_member_candidates(node_member)
342
- ruby_supported_method = ruby_candidates.find { |m| ruby_methods.include?(m) }
343
-
344
- total += 1
345
- if ruby_supported_method
346
- supported += 1
347
- end
348
-
349
- ruby_ref = "-"
350
- if ruby_owner_const && ruby_candidates.any?
351
- separator = ruby_kind == :module ? "." : "#"
352
- ruby_method_name = ruby_supported_method || ruby_candidates.first
353
- ruby_ref = "#{ruby_owner_const}#{separator}#{ruby_method_name}"
354
- end
355
-
356
- status = ruby_supported_method ? "✅" : "❌"
357
- section_lines << "| `#{node_ref}` | `#{ruby_ref}` | #{status} |"
358
- end
359
-
360
- section_lines << ""
361
- sections << section_lines.join("\n")
362
- end
363
-
364
- coverage = total.zero? ? 0.0 : (supported.to_f / total) * 100.0
365
- lines = []
366
- lines << "# API Coverage"
367
- lines << ""
368
- lines << "- Puppeteer commit: `#{puppeteer_commit}`"
369
- lines << "- Generated by: `development/generate_api_coverage.rb`"
370
- lines << "- Coverage: `#{supported}/#{total}` (`#{format("%.2f", coverage)}%`)"
371
- lines << ""
372
- lines << sections.join("\n")
373
-
374
- lines.join("\n") + "\n"
375
- end
376
-
377
- options = {
378
- puppeteer_dir: ROOT.join("development", "puppeteer").to_s,
379
- cache_dir: ROOT.join("development", "cache").to_s,
380
- output: ROOT.join("API_COVERAGE.md").to_s
381
- }
382
-
383
- OptionParser.new do |opts|
384
- opts.banner = "Usage: bundle exec ruby development/generate_api_coverage.rb [options]"
385
- opts.on("--puppeteer-dir=PATH", "Path to a checked out puppeteer/puppeteer repo") { |v| options[:puppeteer_dir] = v }
386
- opts.on("--cache-dir=PATH", "Cache directory (default: development/cache)") { |v| options[:cache_dir] = v }
387
- opts.on("--output=PATH", "Output path (default: API_COVERAGE.md)") { |v| options[:output] = v }
388
- end.parse!
389
-
390
- puppeteer_dir = Pathname.new(options.fetch(:puppeteer_dir)).expand_path
391
- cache_dir = Pathname.new(options.fetch(:cache_dir)).expand_path
392
- output_path = Pathname.new(options.fetch(:output)).expand_path
393
-
394
- raise "Puppeteer repo not found: #{puppeteer_dir}" unless puppeteer_dir.directory?
395
-
396
- puppeteer_commit = read_puppeteer_commit(puppeteer_dir)
397
- cache_file = cache_dir.join("puppeteer_api.json")
398
-
399
- entries = load_cache(cache_file, expected_commit: puppeteer_commit)
400
- unless entries
401
- entries = extract_puppeteer_api(puppeteer_dir)
402
- write_cache(cache_file, puppeteer_commit: puppeteer_commit, entries: entries)
403
- end
404
-
405
- ruby_api = extract_ruby_public_api
406
- output_path.write(
407
- generate_markdown(
408
- puppeteer_commit: puppeteer_commit,
409
- entries: entries,
410
- ruby_api: ruby_api
411
- )
412
- )
@@ -1 +0,0 @@
1
- 7d750c25cb29764f2fb31cb90b750a8eec350199