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,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
module Core
|
|
6
|
+
# Request represents a network request
|
|
7
|
+
class Request < EventEmitter
|
|
8
|
+
include Disposable::DisposableMixin
|
|
9
|
+
|
|
10
|
+
# Create a request instance from a beforeRequestSent event
|
|
11
|
+
# @param browsing_context [BrowsingContext] The browsing context
|
|
12
|
+
# @param event [Hash] The beforeRequestSent event data
|
|
13
|
+
# @return [Request] New request instance
|
|
14
|
+
def self.from(browsing_context, event)
|
|
15
|
+
request = new(browsing_context, event)
|
|
16
|
+
request.send(:initialize_request)
|
|
17
|
+
request
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :browsing_context, :error, :response
|
|
21
|
+
|
|
22
|
+
def initialize(browsing_context, event)
|
|
23
|
+
super()
|
|
24
|
+
@browsing_context = browsing_context
|
|
25
|
+
@event = event
|
|
26
|
+
@error = nil
|
|
27
|
+
@redirect = nil
|
|
28
|
+
@response = nil
|
|
29
|
+
@response_content_promise = nil
|
|
30
|
+
@request_body_promise = nil
|
|
31
|
+
@disposables = Disposable::DisposableStack.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get request ID
|
|
35
|
+
# @return [String] Request ID
|
|
36
|
+
def id
|
|
37
|
+
@event.dig('request', 'request')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get request URL
|
|
41
|
+
# @return [String] Request URL
|
|
42
|
+
def url
|
|
43
|
+
@event.dig('request', 'url')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get request method
|
|
47
|
+
# @return [String] Request method (GET, POST, etc.)
|
|
48
|
+
def method
|
|
49
|
+
@event.dig('request', 'method')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get request headers
|
|
53
|
+
# @return [Array<Hash>] Request headers
|
|
54
|
+
def headers
|
|
55
|
+
@event.dig('request', 'headers') || []
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get navigation ID if this is a navigation request
|
|
59
|
+
# @return [String, nil] Navigation ID
|
|
60
|
+
def navigation
|
|
61
|
+
@event['navigation']
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get redirect request if this request was redirected
|
|
65
|
+
# @return [Request, nil] Redirect request
|
|
66
|
+
def redirect
|
|
67
|
+
@redirect
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get the last redirect in the chain
|
|
71
|
+
# @return [Request, nil] Last redirect request
|
|
72
|
+
def last_redirect
|
|
73
|
+
redirect_request = @redirect
|
|
74
|
+
while redirect_request
|
|
75
|
+
break unless redirect_request.redirect
|
|
76
|
+
redirect_request = redirect_request.redirect
|
|
77
|
+
end
|
|
78
|
+
redirect_request
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get request initiator information
|
|
82
|
+
# @return [Hash, nil] Initiator info
|
|
83
|
+
def initiator
|
|
84
|
+
initiator_data = @event['initiator']
|
|
85
|
+
return nil unless initiator_data
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
**initiator_data,
|
|
89
|
+
url: @event.dig('request', 'goog:resourceInitiator', 'url'),
|
|
90
|
+
stack: @event.dig('request', 'goog:resourceInitiator', 'stack')
|
|
91
|
+
}.compact
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Check if the request is blocked
|
|
95
|
+
# @return [Boolean] Whether the request is blocked
|
|
96
|
+
def blocked?
|
|
97
|
+
@event['isBlocked'] == true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get resource type (non-standard)
|
|
101
|
+
# @return [String, nil] Resource type
|
|
102
|
+
def resource_type
|
|
103
|
+
@event.dig('request', 'goog:resourceType')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get POST data (non-standard)
|
|
107
|
+
# @return [String, nil] POST data
|
|
108
|
+
def post_data
|
|
109
|
+
@event.dig('request', 'goog:postData')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check if request has POST data
|
|
113
|
+
# @return [Boolean] Whether request has POST data
|
|
114
|
+
def has_post_data?
|
|
115
|
+
(@event.dig('request', 'bodySize') || 0) > 0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get timing information
|
|
119
|
+
# @return [Hash] Timing info
|
|
120
|
+
def timing
|
|
121
|
+
@event.dig('request', 'timings') || {}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Continue the request with optional modifications
|
|
125
|
+
# @param url [String, nil] Modified URL
|
|
126
|
+
# @param method [String, nil] Modified method
|
|
127
|
+
# @param headers [Array<Hash>, nil] Modified headers
|
|
128
|
+
# @param cookies [Array<Hash>, nil] Modified cookies
|
|
129
|
+
# @param body [Hash, nil] Modified body
|
|
130
|
+
def continue_request(url: nil, method: nil, headers: nil, cookies: nil, body: nil)
|
|
131
|
+
params = { request: id }
|
|
132
|
+
params[:url] = url if url
|
|
133
|
+
params[:method] = method if method
|
|
134
|
+
params[:headers] = headers if headers
|
|
135
|
+
params[:cookies] = cookies if cookies
|
|
136
|
+
params[:body] = body if body
|
|
137
|
+
|
|
138
|
+
session.send_command('network.continueRequest', params)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Fail the request
|
|
142
|
+
def fail_request
|
|
143
|
+
session.send_command('network.failRequest', { request: id })
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Provide a response for the request
|
|
147
|
+
# @param status_code [Integer, nil] Response status code
|
|
148
|
+
# @param reason_phrase [String, nil] Response reason phrase
|
|
149
|
+
# @param headers [Array<Hash>, nil] Response headers
|
|
150
|
+
# @param body [Hash, nil] Response body
|
|
151
|
+
def provide_response(status_code: nil, reason_phrase: nil, headers: nil, body: nil)
|
|
152
|
+
params = { request: id }
|
|
153
|
+
params[:statusCode] = status_code if status_code
|
|
154
|
+
params[:reasonPhrase] = reason_phrase if reason_phrase
|
|
155
|
+
params[:headers] = headers if headers
|
|
156
|
+
params[:body] = body if body
|
|
157
|
+
|
|
158
|
+
session.send_command('network.provideResponse', params)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Fetch POST data for the request
|
|
162
|
+
# @return [String, nil] POST data
|
|
163
|
+
def fetch_post_data
|
|
164
|
+
return nil unless has_post_data?
|
|
165
|
+
return @request_body_promise if @request_body_promise
|
|
166
|
+
|
|
167
|
+
@request_body_promise = begin
|
|
168
|
+
result = session.send_command('network.getData', {
|
|
169
|
+
dataType: 'request',
|
|
170
|
+
request: id
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
bytes = result['bytes']
|
|
174
|
+
if bytes['type'] == 'string'
|
|
175
|
+
bytes['value']
|
|
176
|
+
else
|
|
177
|
+
raise "Collected request body data of type #{bytes['type']} is not supported"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get response content
|
|
183
|
+
# @return [String] Response content as binary string
|
|
184
|
+
def response_content
|
|
185
|
+
return @response_content_promise if @response_content_promise
|
|
186
|
+
|
|
187
|
+
@response_content_promise = begin
|
|
188
|
+
result = session.send_command('network.getData', {
|
|
189
|
+
dataType: 'response',
|
|
190
|
+
request: id
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
bytes = result['bytes']
|
|
194
|
+
if bytes['type'] == 'base64'
|
|
195
|
+
[bytes['value']].pack('m0')
|
|
196
|
+
else
|
|
197
|
+
bytes['value']
|
|
198
|
+
end
|
|
199
|
+
rescue => e
|
|
200
|
+
if e.message.include?('No resource with given identifier found')
|
|
201
|
+
raise 'Could not load response body for this request. This might happen if the request is a preflight request.'
|
|
202
|
+
end
|
|
203
|
+
raise
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Continue with authentication
|
|
208
|
+
# @param action [String] 'provideCredentials', 'default', or 'cancel'
|
|
209
|
+
# @param credentials [Hash, nil] Credentials hash with username and password
|
|
210
|
+
def continue_with_auth(action:, credentials: nil)
|
|
211
|
+
params = {
|
|
212
|
+
request: id,
|
|
213
|
+
action: action
|
|
214
|
+
}
|
|
215
|
+
params[:credentials] = credentials if action == 'provideCredentials'
|
|
216
|
+
|
|
217
|
+
session.send_command('network.continueWithAuth', params)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
protected
|
|
221
|
+
|
|
222
|
+
def perform_dispose
|
|
223
|
+
@disposables.dispose
|
|
224
|
+
super
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def session
|
|
230
|
+
@browsing_context.user_context.browser.session
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def initialize_request
|
|
234
|
+
# Listen for browsing context closure
|
|
235
|
+
@browsing_context.once(:closed) do |data|
|
|
236
|
+
@error = data[:reason]
|
|
237
|
+
emit(:error, @error)
|
|
238
|
+
dispose
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Listen for redirect
|
|
242
|
+
session.on('network.beforeRequestSent') do |event|
|
|
243
|
+
next unless event['context'] == @browsing_context.id
|
|
244
|
+
next unless event.dig('request', 'request') == id
|
|
245
|
+
|
|
246
|
+
# Check if this is a redirect
|
|
247
|
+
previous_has_auth = @event.dig('request', 'headers')&.any? do |h|
|
|
248
|
+
h['name'].downcase == 'authorization'
|
|
249
|
+
end
|
|
250
|
+
new_has_auth = event.dig('request', 'headers')&.any? do |h|
|
|
251
|
+
h['name'].downcase == 'authorization'
|
|
252
|
+
end
|
|
253
|
+
is_after_auth = new_has_auth && !previous_has_auth
|
|
254
|
+
|
|
255
|
+
next unless event['redirectCount'] == @event['redirectCount'] + 1 || is_after_auth
|
|
256
|
+
|
|
257
|
+
@redirect = Request.from(@browsing_context, event)
|
|
258
|
+
emit(:redirect, @redirect)
|
|
259
|
+
dispose
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Listen for authentication required
|
|
263
|
+
session.on('network.authRequired') do |event|
|
|
264
|
+
next unless event['context'] == @browsing_context.id
|
|
265
|
+
next unless event.dig('request', 'request') == id
|
|
266
|
+
next unless event['isBlocked']
|
|
267
|
+
|
|
268
|
+
emit(:authenticate, nil)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Listen for fetch error
|
|
272
|
+
session.on('network.fetchError') do |event|
|
|
273
|
+
next unless event['context'] == @browsing_context.id
|
|
274
|
+
next unless event.dig('request', 'request') == id
|
|
275
|
+
next unless event['redirectCount'] == @event['redirectCount']
|
|
276
|
+
|
|
277
|
+
@error = event['errorText']
|
|
278
|
+
emit(:error, @error)
|
|
279
|
+
dispose
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Listen for response completed
|
|
283
|
+
session.on('network.responseCompleted') do |event|
|
|
284
|
+
next unless event['context'] == @browsing_context.id
|
|
285
|
+
next unless event.dig('request', 'request') == id
|
|
286
|
+
next unless event['redirectCount'] == @event['redirectCount']
|
|
287
|
+
|
|
288
|
+
@response = event['response']
|
|
289
|
+
@event['request']['timings'] = event.dig('request', 'timings')
|
|
290
|
+
emit(:success, @response)
|
|
291
|
+
|
|
292
|
+
# Don't dispose if this is a redirect
|
|
293
|
+
status = @response['status']
|
|
294
|
+
dispose unless status >= 300 && status < 400
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
module Core
|
|
6
|
+
# Session represents a BiDi session with the browser
|
|
7
|
+
# It wraps a Connection and provides session-specific functionality
|
|
8
|
+
class Session < EventEmitter
|
|
9
|
+
include Disposable::DisposableMixin
|
|
10
|
+
|
|
11
|
+
# Create a new session from an existing connection
|
|
12
|
+
# @param connection [Puppeteer::Bidi::Connection] The BiDi connection
|
|
13
|
+
# @param capabilities [Hash] Session capabilities
|
|
14
|
+
# @return [Session] New session instance
|
|
15
|
+
def self.from(connection:, capabilities:)
|
|
16
|
+
Async do
|
|
17
|
+
result = connection.async_send_command('session.new', { capabilities: capabilities }).wait
|
|
18
|
+
session = new(connection, result)
|
|
19
|
+
session.send(:initialize_session).wait
|
|
20
|
+
session
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :connection, :id, :capabilities
|
|
25
|
+
attr_accessor :browser
|
|
26
|
+
|
|
27
|
+
def initialize(connection, info)
|
|
28
|
+
super()
|
|
29
|
+
@connection = connection
|
|
30
|
+
@info = info
|
|
31
|
+
@id = info['sessionId']
|
|
32
|
+
@capabilities = info['capabilities']
|
|
33
|
+
@reason = nil
|
|
34
|
+
@disposables = Disposable::DisposableStack.new
|
|
35
|
+
|
|
36
|
+
# Forward BiDi events from connection to session
|
|
37
|
+
setup_event_forwarding
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if the session has ended
|
|
41
|
+
def ended?
|
|
42
|
+
!@reason.nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias disposed? ended?
|
|
46
|
+
|
|
47
|
+
# Send a BiDi command through this session
|
|
48
|
+
# @param method [String] BiDi method name
|
|
49
|
+
# @param params [Hash] Command parameters
|
|
50
|
+
# @return [Hash] Command result
|
|
51
|
+
def async_send_command(method, params = {})
|
|
52
|
+
raise SessionEndedError, @reason if ended?
|
|
53
|
+
@connection.async_send_command(method, params)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Subscribe to BiDi events
|
|
57
|
+
# @param events [Array<String>] Event names to subscribe to
|
|
58
|
+
# @param contexts [Array<String>, nil] Context IDs (optional)
|
|
59
|
+
def subscribe(events, contexts = nil)
|
|
60
|
+
raise SessionEndedError, @reason if ended?
|
|
61
|
+
params = { events: events }
|
|
62
|
+
params[:contexts] = contexts if contexts
|
|
63
|
+
async_send_command('session.subscribe', params)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Add intercepts (same as subscribe but for interception events)
|
|
67
|
+
# @param events [Array<String>] Event names to intercept
|
|
68
|
+
# @param contexts [Array<String>, nil] Context IDs (optional)
|
|
69
|
+
def add_intercepts(events, contexts = nil)
|
|
70
|
+
subscribe(events, contexts)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# End the session
|
|
74
|
+
def end_session
|
|
75
|
+
return if ended?
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
send_command('session.end', {})
|
|
79
|
+
ensure
|
|
80
|
+
dispose_session('Session ended')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
protected
|
|
85
|
+
|
|
86
|
+
def perform_dispose
|
|
87
|
+
@reason ||= 'Session destroyed, probably because the connection broke'
|
|
88
|
+
emit(:ended, { reason: @reason })
|
|
89
|
+
@disposables.dispose
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def initialize_session
|
|
96
|
+
# Subscribe to BiDi modules
|
|
97
|
+
# Based on Puppeteer's subscribeModules: browsingContext, network, log, script, input
|
|
98
|
+
subscribe_modules = %w[
|
|
99
|
+
browsingContext
|
|
100
|
+
network
|
|
101
|
+
log
|
|
102
|
+
script
|
|
103
|
+
input
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
subscribe(subscribe_modules)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def dispose_session(reason)
|
|
110
|
+
@reason = reason
|
|
111
|
+
dispose
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def setup_event_forwarding
|
|
115
|
+
# Forward all BiDi events from connection to this session
|
|
116
|
+
# The existing Connection class uses #on method for event handling
|
|
117
|
+
# We need to set up listeners for all possible BiDi events
|
|
118
|
+
|
|
119
|
+
# For now, we'll use a workaround: store the connection's event listeners
|
|
120
|
+
# and forward to our EventEmitter
|
|
121
|
+
|
|
122
|
+
# List of common BiDi events to forward
|
|
123
|
+
bidi_events = [
|
|
124
|
+
'browsingContext.contextCreated',
|
|
125
|
+
'browsingContext.contextDestroyed',
|
|
126
|
+
'browsingContext.navigationStarted',
|
|
127
|
+
'browsingContext.fragmentNavigated',
|
|
128
|
+
'browsingContext.domContentLoaded',
|
|
129
|
+
'browsingContext.load',
|
|
130
|
+
'browsingContext.historyUpdated',
|
|
131
|
+
'browsingContext.userPromptOpened',
|
|
132
|
+
'browsingContext.userPromptClosed',
|
|
133
|
+
'network.beforeRequestSent',
|
|
134
|
+
'network.responseStarted',
|
|
135
|
+
'network.responseCompleted',
|
|
136
|
+
'network.fetchError',
|
|
137
|
+
'network.authRequired',
|
|
138
|
+
'script.realmCreated',
|
|
139
|
+
'script.realmDestroyed',
|
|
140
|
+
'log.entryAdded',
|
|
141
|
+
'input.fileDialogOpened',
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
bidi_events.each do |event_name|
|
|
145
|
+
@connection.on(event_name) do |params|
|
|
146
|
+
emit(event_name.to_sym, params)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
module Core
|
|
6
|
+
# UserContext represents an isolated browsing context (like an incognito session)
|
|
7
|
+
class UserContext < EventEmitter
|
|
8
|
+
include Disposable::DisposableMixin
|
|
9
|
+
|
|
10
|
+
DEFAULT = 'default'
|
|
11
|
+
|
|
12
|
+
# Create a user context
|
|
13
|
+
# @param browser [Browser] Parent browser
|
|
14
|
+
# @param id [String] Context ID
|
|
15
|
+
# @return [UserContext] New user context
|
|
16
|
+
def self.create(browser, id)
|
|
17
|
+
context = new(browser, id)
|
|
18
|
+
context.send(:initialize_context)
|
|
19
|
+
context
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :browser, :id
|
|
23
|
+
|
|
24
|
+
def initialize(browser, id)
|
|
25
|
+
super()
|
|
26
|
+
@browser = browser
|
|
27
|
+
@id = id
|
|
28
|
+
@reason = nil
|
|
29
|
+
@disposables = Disposable::DisposableStack.new
|
|
30
|
+
@browsing_contexts = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if the context is closed
|
|
34
|
+
def closed?
|
|
35
|
+
!@reason.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
alias disposed? closed?
|
|
39
|
+
|
|
40
|
+
# Get all browsing contexts in this user context
|
|
41
|
+
# @return [Array<BrowsingContext>] Top-level browsing contexts
|
|
42
|
+
def browsing_contexts
|
|
43
|
+
@browsing_contexts.values
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Create a new browsing context (tab or window)
|
|
47
|
+
# @param type [String] 'tab' or 'window'
|
|
48
|
+
# @param options [Hash] Creation options
|
|
49
|
+
# @option options [BrowsingContext] :reference_context Reference context
|
|
50
|
+
# @return [BrowsingContext] New browsing context
|
|
51
|
+
def create_browsing_context(type, **options)
|
|
52
|
+
raise UserContextClosedError, @reason if closed?
|
|
53
|
+
|
|
54
|
+
params = {
|
|
55
|
+
type: type,
|
|
56
|
+
userContext: @id
|
|
57
|
+
}
|
|
58
|
+
params[:referenceContext] = options[:reference_context].id if options[:reference_context]
|
|
59
|
+
params.merge!(options.except(:reference_context))
|
|
60
|
+
|
|
61
|
+
result = session.async_send_command('browsingContext.create', params).wait
|
|
62
|
+
context_id = result['context']
|
|
63
|
+
|
|
64
|
+
# Since event handling might be async or not working properly,
|
|
65
|
+
# check if the context was already created by the event handler
|
|
66
|
+
browsing_context = @browsing_contexts[context_id]
|
|
67
|
+
|
|
68
|
+
# If not created by event handler, create it manually
|
|
69
|
+
if browsing_context.nil?
|
|
70
|
+
browsing_context = BrowsingContext.from(
|
|
71
|
+
self,
|
|
72
|
+
nil, # parent
|
|
73
|
+
context_id,
|
|
74
|
+
'about:blank', # Initial URL
|
|
75
|
+
nil # originalOpener
|
|
76
|
+
)
|
|
77
|
+
@browsing_contexts[context_id] = browsing_context
|
|
78
|
+
|
|
79
|
+
browsing_context.once(:closed) do
|
|
80
|
+
@browsing_contexts.delete(context_id)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
browsing_context
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Remove this user context
|
|
88
|
+
def remove
|
|
89
|
+
return if closed?
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
session.async_send_command('browser.removeUserContext', { userContext: @id })
|
|
93
|
+
ensure
|
|
94
|
+
dispose_context('User context removed')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get cookies for this user context
|
|
99
|
+
# @param options [Hash] Cookie filter options
|
|
100
|
+
# @option options [String] :source_origin Source origin
|
|
101
|
+
# @return [Array<Hash>] Cookies
|
|
102
|
+
def get_cookies(**options)
|
|
103
|
+
raise UserContextClosedError, @reason if closed?
|
|
104
|
+
|
|
105
|
+
source_origin = options.delete(:source_origin)
|
|
106
|
+
params = options.dup
|
|
107
|
+
params[:partition] = {
|
|
108
|
+
type: 'storageKey',
|
|
109
|
+
userContext: @id
|
|
110
|
+
}
|
|
111
|
+
params[:partition][:sourceOrigin] = source_origin if source_origin
|
|
112
|
+
|
|
113
|
+
result = session.async_send_command('storage.getCookies', params)
|
|
114
|
+
result['cookies']
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Set a cookie in this user context
|
|
118
|
+
# @param cookie [Hash] Cookie data
|
|
119
|
+
# @option options [String] :source_origin Source origin
|
|
120
|
+
def set_cookie(cookie, **options)
|
|
121
|
+
raise UserContextClosedError, @reason if closed?
|
|
122
|
+
|
|
123
|
+
source_origin = options[:source_origin]
|
|
124
|
+
params = {
|
|
125
|
+
cookie: cookie,
|
|
126
|
+
partition: {
|
|
127
|
+
type: 'storageKey',
|
|
128
|
+
userContext: @id
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
params[:partition][:sourceOrigin] = source_origin if source_origin
|
|
132
|
+
|
|
133
|
+
session.async_send_command('storage.setCookie', params)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Set permissions for an origin
|
|
137
|
+
# @param origin [String] Origin URL
|
|
138
|
+
# @param descriptor [Hash] Permission descriptor
|
|
139
|
+
# @param state [String] Permission state
|
|
140
|
+
def set_permissions(origin, descriptor, state)
|
|
141
|
+
raise UserContextClosedError, @reason if closed?
|
|
142
|
+
|
|
143
|
+
session.async_send_command('permissions.setPermission', {
|
|
144
|
+
origin: origin,
|
|
145
|
+
descriptor: descriptor,
|
|
146
|
+
state: state,
|
|
147
|
+
userContext: @id
|
|
148
|
+
})
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
protected
|
|
152
|
+
|
|
153
|
+
def perform_dispose
|
|
154
|
+
@reason ||= 'User context closed, probably because the browser disconnected'
|
|
155
|
+
emit(:closed, { reason: @reason })
|
|
156
|
+
@disposables.dispose
|
|
157
|
+
super
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def session
|
|
163
|
+
@browser.session
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def initialize_context
|
|
167
|
+
# Listen for browser closure/disconnection
|
|
168
|
+
@browser.once(:closed) do |data|
|
|
169
|
+
dispose_context("User context closed: #{data[:reason]}")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
@browser.once(:disconnected) do |data|
|
|
173
|
+
dispose_context("User context closed: #{data[:reason]}")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Listen for browsing context creation
|
|
177
|
+
session.on(:'browsingContext.contextCreated') do |info|
|
|
178
|
+
# Only handle top-level contexts (no parent)
|
|
179
|
+
next if info['parent']
|
|
180
|
+
next if info['userContext'] != @id
|
|
181
|
+
|
|
182
|
+
browsing_context = BrowsingContext.from(
|
|
183
|
+
self,
|
|
184
|
+
nil, # parent
|
|
185
|
+
info['context'],
|
|
186
|
+
info['url'],
|
|
187
|
+
info['originalOpener']
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@browsing_contexts[browsing_context.id] = browsing_context
|
|
191
|
+
|
|
192
|
+
# Listen for context closure
|
|
193
|
+
browsing_context.once(:closed) do
|
|
194
|
+
@browsing_contexts.delete(browsing_context.id)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
emit(:browsingcontext, { browsing_context: browsing_context })
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def dispose_context(reason)
|
|
202
|
+
@reason = reason
|
|
203
|
+
dispose
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|