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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CLAUDE/README.md +158 -0
- data/CLAUDE/async_programming.md +158 -0
- data/CLAUDE/click_implementation.md +340 -0
- data/CLAUDE/core_layer_gotchas.md +136 -0
- data/CLAUDE/error_handling.md +232 -0
- data/CLAUDE/file_chooser.md +95 -0
- data/CLAUDE/frame_architecture.md +346 -0
- data/CLAUDE/javascript_evaluation.md +341 -0
- data/CLAUDE/jshandle_implementation.md +505 -0
- data/CLAUDE/keyboard_implementation.md +250 -0
- data/CLAUDE/mouse_implementation.md +140 -0
- data/CLAUDE/navigation_waiting.md +234 -0
- data/CLAUDE/porting_puppeteer.md +214 -0
- data/CLAUDE/query_handler.md +194 -0
- data/CLAUDE/rspec_pending_vs_skip.md +262 -0
- data/CLAUDE/selector_evaluation.md +198 -0
- data/CLAUDE/test_server_routes.md +263 -0
- data/CLAUDE/testing_strategy.md +236 -0
- data/CLAUDE/two_layer_architecture.md +180 -0
- data/CLAUDE/wrapped_element_click.md +247 -0
- data/CLAUDE.md +185 -0
- data/LICENSE.txt +21 -0
- data/README.md +488 -0
- data/Rakefile +21 -0
- data/lib/puppeteer/bidi/async_utils.rb +151 -0
- data/lib/puppeteer/bidi/browser.rb +285 -0
- data/lib/puppeteer/bidi/browser_context.rb +53 -0
- data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
- data/lib/puppeteer/bidi/connection.rb +182 -0
- data/lib/puppeteer/bidi/core/README.md +169 -0
- data/lib/puppeteer/bidi/core/browser.rb +230 -0
- data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
- data/lib/puppeteer/bidi/core/disposable.rb +69 -0
- data/lib/puppeteer/bidi/core/errors.rb +64 -0
- data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
- data/lib/puppeteer/bidi/core/navigation.rb +128 -0
- data/lib/puppeteer/bidi/core/realm.rb +315 -0
- data/lib/puppeteer/bidi/core/request.rb +300 -0
- data/lib/puppeteer/bidi/core/session.rb +153 -0
- data/lib/puppeteer/bidi/core/user_context.rb +208 -0
- data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
- data/lib/puppeteer/bidi/core.rb +45 -0
- data/lib/puppeteer/bidi/deserializer.rb +132 -0
- data/lib/puppeteer/bidi/element_handle.rb +602 -0
- data/lib/puppeteer/bidi/errors.rb +42 -0
- data/lib/puppeteer/bidi/file_chooser.rb +52 -0
- data/lib/puppeteer/bidi/frame.rb +597 -0
- data/lib/puppeteer/bidi/http_response.rb +23 -0
- data/lib/puppeteer/bidi/injected.js +1 -0
- data/lib/puppeteer/bidi/injected_source.rb +21 -0
- data/lib/puppeteer/bidi/js_handle.rb +302 -0
- data/lib/puppeteer/bidi/keyboard.rb +265 -0
- data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
- data/lib/puppeteer/bidi/mouse.rb +170 -0
- data/lib/puppeteer/bidi/page.rb +613 -0
- data/lib/puppeteer/bidi/query_handler.rb +397 -0
- data/lib/puppeteer/bidi/realm.rb +242 -0
- data/lib/puppeteer/bidi/serializer.rb +139 -0
- data/lib/puppeteer/bidi/target.rb +81 -0
- data/lib/puppeteer/bidi/task_manager.rb +44 -0
- data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
- data/lib/puppeteer/bidi/transport.rb +129 -0
- data/lib/puppeteer/bidi/version.rb +7 -0
- data/lib/puppeteer/bidi/wait_task.rb +322 -0
- data/lib/puppeteer/bidi.rb +49 -0
- data/scripts/update_injected_source.rb +57 -0
- data/sig/puppeteer/bidi/browser.rbs +80 -0
- data/sig/puppeteer/bidi/element_handle.rbs +238 -0
- data/sig/puppeteer/bidi/frame.rbs +205 -0
- data/sig/puppeteer/bidi/js_handle.rbs +90 -0
- data/sig/puppeteer/bidi/page.rbs +247 -0
- data/sig/puppeteer/bidi.rbs +15 -0
- 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
|