puppeteer-bidi 0.0.1.beta1

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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +13 -0
  4. data/CLAUDE/README.md +158 -0
  5. data/CLAUDE/async_programming.md +158 -0
  6. data/CLAUDE/click_implementation.md +340 -0
  7. data/CLAUDE/core_layer_gotchas.md +136 -0
  8. data/CLAUDE/error_handling.md +232 -0
  9. data/CLAUDE/file_chooser.md +95 -0
  10. data/CLAUDE/frame_architecture.md +346 -0
  11. data/CLAUDE/javascript_evaluation.md +341 -0
  12. data/CLAUDE/jshandle_implementation.md +505 -0
  13. data/CLAUDE/keyboard_implementation.md +250 -0
  14. data/CLAUDE/mouse_implementation.md +140 -0
  15. data/CLAUDE/navigation_waiting.md +234 -0
  16. data/CLAUDE/porting_puppeteer.md +214 -0
  17. data/CLAUDE/query_handler.md +194 -0
  18. data/CLAUDE/rspec_pending_vs_skip.md +262 -0
  19. data/CLAUDE/selector_evaluation.md +198 -0
  20. data/CLAUDE/test_server_routes.md +263 -0
  21. data/CLAUDE/testing_strategy.md +236 -0
  22. data/CLAUDE/two_layer_architecture.md +180 -0
  23. data/CLAUDE/wrapped_element_click.md +247 -0
  24. data/CLAUDE.md +185 -0
  25. data/LICENSE.txt +21 -0
  26. data/README.md +488 -0
  27. data/Rakefile +21 -0
  28. data/lib/puppeteer/bidi/async_utils.rb +151 -0
  29. data/lib/puppeteer/bidi/browser.rb +285 -0
  30. data/lib/puppeteer/bidi/browser_context.rb +53 -0
  31. data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
  32. data/lib/puppeteer/bidi/connection.rb +182 -0
  33. data/lib/puppeteer/bidi/core/README.md +169 -0
  34. data/lib/puppeteer/bidi/core/browser.rb +230 -0
  35. data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
  36. data/lib/puppeteer/bidi/core/disposable.rb +69 -0
  37. data/lib/puppeteer/bidi/core/errors.rb +64 -0
  38. data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
  39. data/lib/puppeteer/bidi/core/navigation.rb +128 -0
  40. data/lib/puppeteer/bidi/core/realm.rb +315 -0
  41. data/lib/puppeteer/bidi/core/request.rb +300 -0
  42. data/lib/puppeteer/bidi/core/session.rb +153 -0
  43. data/lib/puppeteer/bidi/core/user_context.rb +208 -0
  44. data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
  45. data/lib/puppeteer/bidi/core.rb +45 -0
  46. data/lib/puppeteer/bidi/deserializer.rb +132 -0
  47. data/lib/puppeteer/bidi/element_handle.rb +602 -0
  48. data/lib/puppeteer/bidi/errors.rb +42 -0
  49. data/lib/puppeteer/bidi/file_chooser.rb +52 -0
  50. data/lib/puppeteer/bidi/frame.rb +597 -0
  51. data/lib/puppeteer/bidi/http_response.rb +23 -0
  52. data/lib/puppeteer/bidi/injected.js +1 -0
  53. data/lib/puppeteer/bidi/injected_source.rb +21 -0
  54. data/lib/puppeteer/bidi/js_handle.rb +302 -0
  55. data/lib/puppeteer/bidi/keyboard.rb +265 -0
  56. data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
  57. data/lib/puppeteer/bidi/mouse.rb +170 -0
  58. data/lib/puppeteer/bidi/page.rb +613 -0
  59. data/lib/puppeteer/bidi/query_handler.rb +397 -0
  60. data/lib/puppeteer/bidi/realm.rb +242 -0
  61. data/lib/puppeteer/bidi/serializer.rb +139 -0
  62. data/lib/puppeteer/bidi/target.rb +81 -0
  63. data/lib/puppeteer/bidi/task_manager.rb +44 -0
  64. data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
  65. data/lib/puppeteer/bidi/transport.rb +129 -0
  66. data/lib/puppeteer/bidi/version.rb +7 -0
  67. data/lib/puppeteer/bidi/wait_task.rb +322 -0
  68. data/lib/puppeteer/bidi.rb +49 -0
  69. data/scripts/update_injected_source.rb +57 -0
  70. data/sig/puppeteer/bidi/browser.rbs +80 -0
  71. data/sig/puppeteer/bidi/element_handle.rbs +238 -0
  72. data/sig/puppeteer/bidi/frame.rbs +205 -0
  73. data/sig/puppeteer/bidi/js_handle.rbs +90 -0
  74. data/sig/puppeteer/bidi/page.rbs +247 -0
  75. data/sig/puppeteer/bidi.rbs +15 -0
  76. metadata +176 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'puppeteer/bidi/lazy_arg'
5
+
6
+ module Puppeteer
7
+ module Bidi
8
+ # Serializer converts Ruby values to BiDi Script.LocalValue format
9
+ # Based on Puppeteer's Serializer.ts
10
+ class Serializer
11
+ class << self
12
+ # Serialize a Ruby value to BiDi LocalValue format
13
+ # @param value [Object] Ruby value to serialize
14
+ # @return [Hash] BiDi LocalValue
15
+ # @raise [ArgumentError] for unsupported types or circular references
16
+ def serialize(value)
17
+ value = value.resolve while value.is_a?(LazyArg)
18
+
19
+ # Check for circular references first for complex objects
20
+ if complex_object?(value)
21
+ check_circular_reference(value)
22
+ end
23
+
24
+ case value
25
+ when JSHandle
26
+ # Handle references (either handle or sharedId)
27
+ serialize_handle(value)
28
+ when String
29
+ { type: 'string', value: value }
30
+ when Integer
31
+ { type: 'number', value: value }
32
+ when Float
33
+ serialize_number(value)
34
+ when TrueClass, FalseClass
35
+ { type: 'boolean', value: value }
36
+ when NilClass
37
+ { type: 'null' }
38
+ when Symbol
39
+ raise ArgumentError, 'Unable to serialize Symbol'
40
+ when Proc, Method
41
+ raise ArgumentError, 'Unable to serialize Proc/Function'
42
+ when Array
43
+ {
44
+ type: 'array',
45
+ value: value.map { |item| serialize(item) }
46
+ }
47
+ when Hash
48
+ # Plain Ruby Hash → BiDi object with [key, value] pairs
49
+ {
50
+ type: 'object',
51
+ value: value.map { |k, v| [k.to_s, serialize(v)] }
52
+ }
53
+ when Set
54
+ {
55
+ type: 'set',
56
+ value: value.map { |item| serialize(item) }
57
+ }
58
+ when Regexp
59
+ {
60
+ type: 'regexp',
61
+ value: {
62
+ pattern: value.source,
63
+ flags: regexp_flags(value)
64
+ }
65
+ }
66
+ when Time, Date, DateTime
67
+ # Convert to ISO 8601 string
68
+ time_value = value.is_a?(Time) ? value : value.to_time
69
+ {
70
+ type: 'date',
71
+ value: time_value.utc.iso8601(3)
72
+ }
73
+ else
74
+ # Unsupported type
75
+ raise ArgumentError, "Unable to serialize #{value.class}. Use plain objects instead"
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ # Check if value is a complex object that might have circular references
82
+ def complex_object?(value)
83
+ value.is_a?(Array) || value.is_a?(Hash) || value.is_a?(Set)
84
+ end
85
+
86
+ # Check for circular references using JSON encoding
87
+ # This matches Puppeteer's approach
88
+ def check_circular_reference(value)
89
+ JSON.generate(value)
90
+ rescue JSON::GeneratorError => e
91
+ if e.message.include?('circular') || e.message.include?('depth')
92
+ raise ArgumentError, 'Recursive objects are not allowed'
93
+ end
94
+ raise
95
+ end
96
+
97
+ # Serialize a JSHandle to a BiDi reference
98
+ def serialize_handle(handle)
99
+ remote_value = handle.remote_value
100
+
101
+ # Prefer handle over sharedId
102
+ if remote_value['sharedId']
103
+ { sharedId: remote_value['sharedId'] }
104
+ elsif remote_value['handle']
105
+ { handle: remote_value['handle'] }
106
+ else
107
+ # Fallback: return the full remote value
108
+ remote_value
109
+ end
110
+ end
111
+
112
+ # Serialize a Float to BiDi number format, handling special values
113
+ def serialize_number(num)
114
+ if num.nan?
115
+ { type: 'number', value: 'NaN' }
116
+ elsif num == Float::INFINITY
117
+ { type: 'number', value: 'Infinity' }
118
+ elsif num == -Float::INFINITY
119
+ { type: 'number', value: '-Infinity' }
120
+ elsif num.zero? && (1.0 / num).negative?
121
+ # Detect -0.0
122
+ { type: 'number', value: '-0' }
123
+ else
124
+ { type: 'number', value: num }
125
+ end
126
+ end
127
+
128
+ # Extract regexp flags from Ruby Regexp
129
+ def regexp_flags(regexp)
130
+ flags = []
131
+ flags << 'i' if (regexp.options & Regexp::IGNORECASE) != 0
132
+ flags << 'm' if (regexp.options & Regexp::MULTILINE) != 0
133
+ flags << 'x' if (regexp.options & Regexp::EXTENDED) != 0
134
+ flags.join
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,81 @@
1
+ module Puppeteer
2
+ module Bidi
3
+ class BrowserTarget
4
+ def initialize(browser)
5
+ @browser = browser
6
+ end
7
+
8
+ def page
9
+ nil
10
+ end
11
+
12
+ def url
13
+ ''
14
+ end
15
+
16
+ def type
17
+ 'browser'
18
+ end
19
+
20
+ def browser
21
+ @browser
22
+ end
23
+
24
+ def browser_context
25
+ @browser.default_browser_context
26
+ end
27
+ end
28
+
29
+ class PageTarget
30
+ def initialize(page)
31
+ @page = page
32
+ end
33
+
34
+ def page
35
+ @page
36
+ end
37
+
38
+ def url
39
+ @page.url
40
+ end
41
+
42
+ def type
43
+ 'page'
44
+ end
45
+
46
+ def browser
47
+ @page.browser_context.browser
48
+ end
49
+
50
+ def browser_context
51
+ @page.browser_context
52
+ end
53
+ end
54
+
55
+ class FrameTarget
56
+ def initialize(frame)
57
+ @frame = frame
58
+ end
59
+
60
+ def page
61
+ @frame.page
62
+ end
63
+
64
+ def url
65
+ @frame.url
66
+ end
67
+
68
+ def type
69
+ 'frame'
70
+ end
71
+
72
+ def browser
73
+ @frame.browser_context.browser
74
+ end
75
+
76
+ def browser_context
77
+ @frame.browser_context
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppeteer
4
+ module Bidi
5
+ # TaskManager tracks active WaitTask instances to enable coordinated lifecycle management
6
+ # This is a faithful port of Puppeteer's TaskManager implementation:
7
+ # https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/common/WaitTask.ts
8
+ class TaskManager
9
+ def initialize
10
+ @tasks = Set.new
11
+ end
12
+
13
+ # Add a task to the manager
14
+ # Corresponds to Puppeteer's add(task: WaitTask<any>): void
15
+ # @param task [WaitTask] Task to add
16
+ def add(task)
17
+ @tasks.add(task)
18
+ end
19
+
20
+ # Delete a task from the manager
21
+ # Corresponds to Puppeteer's delete(task: WaitTask<any>): void
22
+ # @param task [WaitTask] Task to delete
23
+ def delete(task)
24
+ @tasks.delete(task)
25
+ end
26
+
27
+ # Terminate all tasks with an optional error
28
+ # Corresponds to Puppeteer's terminateAll(error?: Error): void
29
+ # @param error [Exception, nil] Error to terminate with
30
+ def terminate_all(error = nil)
31
+ @tasks.each do |task|
32
+ task.terminate(error)
33
+ end
34
+ @tasks.clear
35
+ end
36
+
37
+ # Rerun all tasks in parallel
38
+ # Corresponds to Puppeteer's async rerunAll(): Promise<void>
39
+ def rerun_all
40
+ @tasks.each(&:rerun)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppeteer
4
+ module Bidi
5
+ # Minimal timeout settings helper to share default wait values across realms.
6
+ class TimeoutSettings
7
+ def initialize(default_timeout)
8
+ @default_timeout = default_timeout
9
+ end
10
+
11
+ def timeout
12
+ @default_timeout
13
+ end
14
+
15
+ def set_default_timeout(timeout)
16
+ @default_timeout = timeout
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+ require 'async/websocket/client'
5
+ require 'async/http/endpoint'
6
+ require 'json'
7
+ require 'uri'
8
+
9
+ module Puppeteer
10
+ module Bidi
11
+ # Transport handles WebSocket communication with BiDi server
12
+ # This is the lowest layer that manages raw WebSocket send/receive
13
+ class Transport
14
+ class ClosedError < Error; end
15
+
16
+ attr_reader :url
17
+
18
+ def initialize(url)
19
+ # BiDi WebSocket endpoint requires /session path
20
+ @url = url.end_with?('/session') ? url : "#{url}/session"
21
+ @endpoint = nil
22
+ @connection = nil
23
+ @task = nil
24
+ @connected = false
25
+ @closed = false
26
+ @on_message = nil
27
+ @on_close = nil
28
+ end
29
+
30
+ # Connect to WebSocket and start receiving messages
31
+ def connect
32
+ connection_promise = Async::Promise.new
33
+ @task = Async do |task|
34
+ endpoint = Async::HTTP::Endpoint.parse(@url)
35
+
36
+ # Connect to WebSocket - this matches minibidi's implementation
37
+ Async::WebSocket::Client.connect(endpoint) do |connection|
38
+ @connection = connection
39
+ @connected = true
40
+ connection_promise.resolve(connection)
41
+
42
+ # Start message receiving loop (this will block until connection closes)
43
+ receive_loop(connection)
44
+ end
45
+ rescue => e
46
+ warn "Transport connect error: #{e.class} - #{e.message}"
47
+ warn e.backtrace.join("\n")
48
+ connection_promise.reject(e)
49
+ close
50
+ ensure
51
+ @connected = false
52
+ end
53
+ connection_promise
54
+ end
55
+
56
+ # Send a message to BiDi server
57
+ def async_send_message(message)
58
+ raise ClosedError, 'Transport is closed' if @closed
59
+
60
+ debug_print_send(message)
61
+ json = JSON.generate(message)
62
+ Async do
63
+ @connection&.write(json)
64
+ @connection&.flush
65
+ end
66
+ end
67
+
68
+ # Register message handler
69
+ def on_message(&block)
70
+ @on_message = block
71
+ end
72
+
73
+ # Register close handler
74
+ def on_close(&block)
75
+ @on_close = block
76
+ end
77
+
78
+ # Close the WebSocket connection
79
+ def close
80
+ return if @closed
81
+
82
+ @closed = true
83
+ @connection&.close
84
+ @on_close&.call
85
+ @task&.stop
86
+ end
87
+
88
+ def closed?
89
+ @closed
90
+ end
91
+
92
+ def connected?
93
+ @connected && !@closed
94
+ end
95
+
96
+ private
97
+
98
+ def receive_loop(connection)
99
+ while (message = connection.read)
100
+ next if message.nil?
101
+
102
+ Async do
103
+ data = JSON.parse(message)
104
+ debug_print_receive(data)
105
+ @on_message&.call(data)
106
+ rescue JSON::ParserError => e
107
+ warn "Failed to parse BiDi message: #{e.message}"
108
+ end
109
+ end
110
+ rescue => e
111
+ warn "Transport receive error: #{e.message}"
112
+ ensure
113
+ close unless @closed
114
+ end
115
+
116
+ def debug_print_send(message)
117
+ if %w[1 true].include?(ENV['DEBUG_PROTOCOL'])
118
+ puts "SEND >> #{JSON.generate(message)}"
119
+ end
120
+ end
121
+
122
+ def debug_print_receive(message)
123
+ if %w[1 true].include?(ENV['DEBUG_PROTOCOL'])
124
+ puts "RECV << #{JSON.generate(message)}"
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppeteer
4
+ module Bidi
5
+ VERSION = "0.0.1.beta1"
6
+ end
7
+ end