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,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'async'
|
|
4
|
+
require 'async/promise'
|
|
5
|
+
|
|
6
|
+
module Puppeteer
|
|
7
|
+
module Bidi
|
|
8
|
+
# Connection manages BiDi protocol communication
|
|
9
|
+
# Handles command sending, response waiting, and event dispatching
|
|
10
|
+
class Connection
|
|
11
|
+
class TimeoutError < Error; end
|
|
12
|
+
class ProtocolError < Error; end
|
|
13
|
+
|
|
14
|
+
DEFAULT_TIMEOUT = 30_000 # 30 seconds in milliseconds
|
|
15
|
+
|
|
16
|
+
def initialize(transport)
|
|
17
|
+
@transport = transport
|
|
18
|
+
@next_id = 1
|
|
19
|
+
@pending_commands = {}
|
|
20
|
+
@event_listeners = {}
|
|
21
|
+
@closed = false
|
|
22
|
+
|
|
23
|
+
setup_transport_handlers
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Send a BiDi command and wait for response
|
|
27
|
+
# @param method [String] BiDi method name (e.g., 'browsingContext.navigate')
|
|
28
|
+
# @param params [Hash] Command parameters
|
|
29
|
+
# @param timeout [Integer] Timeout in milliseconds
|
|
30
|
+
# @return [Hash] Command result
|
|
31
|
+
def async_send_command(method, params = {}, timeout: DEFAULT_TIMEOUT)
|
|
32
|
+
raise ProtocolError, 'Connection is closed' if @closed
|
|
33
|
+
|
|
34
|
+
id = next_id
|
|
35
|
+
command = {
|
|
36
|
+
id: id,
|
|
37
|
+
method: method,
|
|
38
|
+
params: params
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Create promise for this command
|
|
42
|
+
promise = Async::Promise.new
|
|
43
|
+
|
|
44
|
+
@pending_commands[id] = {
|
|
45
|
+
promise: promise,
|
|
46
|
+
method: method,
|
|
47
|
+
sent_at: Time.now
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Debug output
|
|
51
|
+
if ENV['DEBUG_BIDI_COMMAND']
|
|
52
|
+
puts "[BiDi] Request #{method}: #{command.inspect}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Async do
|
|
56
|
+
# Send command through transport
|
|
57
|
+
@transport.async_send_message(command).wait
|
|
58
|
+
|
|
59
|
+
# Wait for response with timeout
|
|
60
|
+
begin
|
|
61
|
+
result = AsyncUtils.async_timeout(timeout, promise).wait
|
|
62
|
+
|
|
63
|
+
# Debug output
|
|
64
|
+
if ENV['DEBUG_BIDI_COMMAND']
|
|
65
|
+
puts "[BiDi] Response for #{method}: #{result.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if result['error']
|
|
69
|
+
# BiDi error format: { "error": "error_type", "message": "detailed message", ... }
|
|
70
|
+
error_type = result['error']
|
|
71
|
+
error_message = result['message'] || error_type
|
|
72
|
+
raise ProtocolError, "BiDi error (#{method}): #{error_message}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
result['result']
|
|
76
|
+
rescue Async::TimeoutError
|
|
77
|
+
@pending_commands.delete(id)
|
|
78
|
+
raise TimeoutError, "Timeout waiting for #{method} (#{timeout}ms)"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Subscribe to BiDi events
|
|
84
|
+
# @param event [String] Event name (e.g., 'browsingContext.navigationStarted')
|
|
85
|
+
# @param block [Proc] Event handler
|
|
86
|
+
def on(event, &block)
|
|
87
|
+
@event_listeners[event] ||= []
|
|
88
|
+
@event_listeners[event] << block
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Unsubscribe from BiDi events
|
|
92
|
+
def off(event, &block)
|
|
93
|
+
return unless @event_listeners[event]
|
|
94
|
+
|
|
95
|
+
if block
|
|
96
|
+
@event_listeners[event].delete(block)
|
|
97
|
+
else
|
|
98
|
+
@event_listeners.delete(event)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Close the connection
|
|
103
|
+
def close
|
|
104
|
+
return if @closed
|
|
105
|
+
|
|
106
|
+
@closed = true
|
|
107
|
+
|
|
108
|
+
# Reject all pending commands
|
|
109
|
+
@pending_commands.each_value do |pending|
|
|
110
|
+
pending[:promise].reject(ProtocolError.new('Connection closed'))
|
|
111
|
+
end
|
|
112
|
+
@pending_commands.clear
|
|
113
|
+
|
|
114
|
+
@transport.close
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def closed?
|
|
118
|
+
@closed
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def next_id
|
|
124
|
+
id = @next_id
|
|
125
|
+
@next_id += 1
|
|
126
|
+
id
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def setup_transport_handlers
|
|
130
|
+
@transport.on_message do |message|
|
|
131
|
+
handle_message(message)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@transport.on_close do
|
|
135
|
+
close
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def handle_message(message)
|
|
140
|
+
# Response to a command (has 'id' field)
|
|
141
|
+
if message['id']
|
|
142
|
+
handle_response(message)
|
|
143
|
+
# Event (has 'method' but no 'id')
|
|
144
|
+
elsif message['method']
|
|
145
|
+
handle_event(message)
|
|
146
|
+
else
|
|
147
|
+
warn "Unknown BiDi message format: #{message}"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def handle_response(message)
|
|
152
|
+
id = message['id']
|
|
153
|
+
pending = @pending_commands.delete(id)
|
|
154
|
+
|
|
155
|
+
unless pending
|
|
156
|
+
warn "Received response for unknown command id: #{id}"
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Resolve the promise with the response
|
|
161
|
+
pending[:promise].resolve(message)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def handle_event(message)
|
|
165
|
+
method = message['method']
|
|
166
|
+
params = message['params'] || {}
|
|
167
|
+
|
|
168
|
+
listeners = @event_listeners[method]
|
|
169
|
+
return unless listeners
|
|
170
|
+
|
|
171
|
+
# Call all registered listeners for this event
|
|
172
|
+
listeners.each do |listener|
|
|
173
|
+
begin
|
|
174
|
+
listener.call(params)
|
|
175
|
+
rescue => e
|
|
176
|
+
warn "Error in event listener for #{method}: #{e.message}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Puppeteer BiDi Core
|
|
2
|
+
|
|
3
|
+
The `core` module provides a low-level layer that sits above the WebSocket transport to provide a structured API to WebDriver BiDi's flat API. It provides object-oriented semantics around WebDriver BiDi resources and automatically carries out the correct order of events through the use of events.
|
|
4
|
+
|
|
5
|
+
This implementation is a Ruby port of Puppeteer's BiDi core layer, following the same design principles.
|
|
6
|
+
|
|
7
|
+
## Design Principles
|
|
8
|
+
|
|
9
|
+
The following design decisions should be considered when developing or using the core layer:
|
|
10
|
+
|
|
11
|
+
### 1. Required vs Optional Arguments
|
|
12
|
+
|
|
13
|
+
- **Required arguments** are method parameters
|
|
14
|
+
- **Optional arguments** are keyword arguments
|
|
15
|
+
|
|
16
|
+
This follows Ruby conventions and makes required parameters explicit.
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# Required parameter first, optional as keyword arguments
|
|
20
|
+
browsing_context.navigate(url, wait: 'complete')
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Session Visibility
|
|
24
|
+
|
|
25
|
+
- The session shall **never be exposed** on any public method except on Browser
|
|
26
|
+
- Private access to session is allowed
|
|
27
|
+
|
|
28
|
+
This prevents obfuscation of the session's origin. By only allowing it on the browser, the origin is well-defined.
|
|
29
|
+
|
|
30
|
+
### 3. WebDriver BiDi Compliance
|
|
31
|
+
|
|
32
|
+
- `core` implements WebDriver BiDi plus its surrounding specifications
|
|
33
|
+
- Always follow the spec, not Puppeteer's needs
|
|
34
|
+
- This allows precise bug identification (spec issue vs implementation issue)
|
|
35
|
+
|
|
36
|
+
### 4. Comprehensive but Minimal
|
|
37
|
+
|
|
38
|
+
- Implements all edges and nodes required by a feature
|
|
39
|
+
- Never skips intermediate nodes or composes edges
|
|
40
|
+
- Ensures WebDriver BiDi semantics are carried out correctly
|
|
41
|
+
|
|
42
|
+
Example: Fragment navigation must flow through Navigation to BrowsingContext, not directly from fragment navigation to BrowsingContext.
|
|
43
|
+
|
|
44
|
+
## Architecture
|
|
45
|
+
|
|
46
|
+
The core module provides these main classes:
|
|
47
|
+
|
|
48
|
+
### Foundation Classes
|
|
49
|
+
|
|
50
|
+
- **EventEmitter**: Event subscription and emission
|
|
51
|
+
- **Disposable**: Resource management and cleanup with DisposableStack
|
|
52
|
+
|
|
53
|
+
### Protocol Classes
|
|
54
|
+
|
|
55
|
+
- **Session**: BiDi session management, wraps Connection
|
|
56
|
+
- **Browser**: Browser instance management
|
|
57
|
+
- **UserContext**: Isolated browsing contexts (similar to incognito)
|
|
58
|
+
- **BrowsingContext**: Individual tabs/windows/frames
|
|
59
|
+
- **Navigation**: Navigation tracking and lifecycle
|
|
60
|
+
- **Request**: Network request management
|
|
61
|
+
- **UserPrompt**: User prompt (alert/confirm/prompt) handling
|
|
62
|
+
|
|
63
|
+
### Execution Context Classes
|
|
64
|
+
|
|
65
|
+
- **Realm**: Base class for JavaScript execution contexts
|
|
66
|
+
- **WindowRealm**: Window/iframe realms
|
|
67
|
+
- **DedicatedWorkerRealm**: Dedicated worker realms
|
|
68
|
+
- **SharedWorkerRealm**: Shared worker realms
|
|
69
|
+
|
|
70
|
+
## Object Hierarchy
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
Browser
|
|
74
|
+
├── Session (wrapped Connection)
|
|
75
|
+
└── UserContext (isolated session)
|
|
76
|
+
└── BrowsingContext (tab/window/frame)
|
|
77
|
+
├── Navigation (navigation lifecycle)
|
|
78
|
+
├── Request (network requests)
|
|
79
|
+
├── UserPrompt (alerts, confirms, prompts)
|
|
80
|
+
└── Realm (JavaScript execution)
|
|
81
|
+
├── WindowRealm (main window/iframe)
|
|
82
|
+
├── DedicatedWorkerRealm (web workers)
|
|
83
|
+
└── SharedWorkerRealm (shared workers)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Usage Example
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
require 'puppeteer/bidi/core'
|
|
90
|
+
|
|
91
|
+
# Create a session from an existing connection
|
|
92
|
+
session = Puppeteer::Bidi::Core::Session.from(connection, capabilities)
|
|
93
|
+
|
|
94
|
+
# Create a browser instance
|
|
95
|
+
browser = Puppeteer::Bidi::Core::Browser.from(session)
|
|
96
|
+
|
|
97
|
+
# Get the default user context
|
|
98
|
+
context = browser.default_user_context
|
|
99
|
+
|
|
100
|
+
# Create a browsing context (tab)
|
|
101
|
+
browsing_context = context.create_browsing_context('tab')
|
|
102
|
+
|
|
103
|
+
# Listen for navigation events
|
|
104
|
+
browsing_context.on(:navigation) do |data|
|
|
105
|
+
navigation = data[:navigation]
|
|
106
|
+
puts "Navigation started to #{browsing_context.url}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Navigate to a URL
|
|
110
|
+
browsing_context.navigate('https://example.com', wait: 'complete')
|
|
111
|
+
|
|
112
|
+
# Evaluate JavaScript in the default realm
|
|
113
|
+
result = browsing_context.default_realm.evaluate('document.title', true)
|
|
114
|
+
puts "Page title: #{result['value']}"
|
|
115
|
+
|
|
116
|
+
# Close the browsing context
|
|
117
|
+
browsing_context.close
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Event Handling
|
|
121
|
+
|
|
122
|
+
All core classes extend `EventEmitter` and emit various events:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# Browser events
|
|
126
|
+
browser.on(:closed) { |data| puts "Browser closed: #{data[:reason]}" }
|
|
127
|
+
browser.on(:disconnected) { |data| puts "Browser disconnected: #{data[:reason]}" }
|
|
128
|
+
|
|
129
|
+
# BrowsingContext events
|
|
130
|
+
browsing_context.on(:navigation) { |data| puts "Navigation: #{data[:navigation]}" }
|
|
131
|
+
browsing_context.on(:request) { |data| puts "Request: #{data[:request].url}" }
|
|
132
|
+
browsing_context.on(:load) { puts "Page loaded" }
|
|
133
|
+
browsing_context.on(:dom_content_loaded) { puts "DOM ready" }
|
|
134
|
+
|
|
135
|
+
# Request events
|
|
136
|
+
request.on(:redirect) { |redirect| puts "Redirected to: #{redirect.url}" }
|
|
137
|
+
request.on(:success) { |response| puts "Response: #{response['status']}" }
|
|
138
|
+
request.on(:error) { |error| puts "Request failed: #{error}" }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Resource Management
|
|
142
|
+
|
|
143
|
+
Core classes implement the `Disposable` pattern for proper resource cleanup:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# Resources are automatically disposed when parent is disposed
|
|
147
|
+
browser.close # Disposes all user contexts, browsing contexts, etc.
|
|
148
|
+
|
|
149
|
+
# Disposal triggers appropriate events
|
|
150
|
+
browsing_context.on(:closed) do |data|
|
|
151
|
+
puts "Context closed: #{data[:reason]}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check disposal status
|
|
155
|
+
puts "Disposed: #{browsing_context.disposed?}"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Differences from TypeScript Implementation
|
|
159
|
+
|
|
160
|
+
1. **Ruby Conventions**: Uses snake_case instead of camelCase
|
|
161
|
+
2. **Keyword Arguments**: Uses Ruby keyword arguments instead of options hashes
|
|
162
|
+
3. **Symbols**: Uses symbols for event names instead of strings
|
|
163
|
+
4. **No Decorators**: Decorators like `@throwIfDisposed` are implemented as method guards
|
|
164
|
+
5. **Async Primitives**: Uses Ruby's Async library (Fiber-based) instead of JavaScript promises (similar to async/await)
|
|
165
|
+
|
|
166
|
+
## References
|
|
167
|
+
|
|
168
|
+
- [WebDriver BiDi Specification](https://w3c.github.io/webdriver-bidi/)
|
|
169
|
+
- [Puppeteer BiDi Core (TypeScript)](https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core/src/bidi/core)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
module Core
|
|
6
|
+
# Browser represents the browser instance in the core layer
|
|
7
|
+
# It manages user contexts and provides browser-level operations
|
|
8
|
+
class Browser < EventEmitter
|
|
9
|
+
include Disposable::DisposableMixin
|
|
10
|
+
|
|
11
|
+
# Create a browser instance from a session
|
|
12
|
+
# @param session [Session] BiDi session
|
|
13
|
+
# @return [Browser] Browser instance
|
|
14
|
+
def self.from(session)
|
|
15
|
+
browser = new(session)
|
|
16
|
+
Async do
|
|
17
|
+
browser.send(:initialize_browser).wait
|
|
18
|
+
browser
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :session
|
|
23
|
+
|
|
24
|
+
def initialize(session)
|
|
25
|
+
super()
|
|
26
|
+
@session = session
|
|
27
|
+
@closed = false
|
|
28
|
+
@reason = nil
|
|
29
|
+
@disposables = Disposable::DisposableStack.new
|
|
30
|
+
@user_contexts = {}
|
|
31
|
+
@shared_workers = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if the browser is closed
|
|
35
|
+
def closed?
|
|
36
|
+
@closed
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check if the browser is disconnected
|
|
40
|
+
def disconnected?
|
|
41
|
+
!@reason.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias disposed? disconnected?
|
|
45
|
+
|
|
46
|
+
# Get the default user context
|
|
47
|
+
# @return [UserContext] Default user context
|
|
48
|
+
def default_user_context
|
|
49
|
+
@user_contexts[UserContext::DEFAULT]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get all user contexts
|
|
53
|
+
# @return [Array<UserContext>] All user contexts
|
|
54
|
+
def user_contexts
|
|
55
|
+
@user_contexts.values
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Close the browser
|
|
59
|
+
def close
|
|
60
|
+
Async do
|
|
61
|
+
return if @closed
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
@session.async_send_command('browser.close', {})
|
|
65
|
+
ensure
|
|
66
|
+
dispose_browser('Browser closed', closed: true)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Add a preload script to the browser
|
|
72
|
+
# @param function_declaration [String] JavaScript function to preload
|
|
73
|
+
# @param options [Hash] Preload script options
|
|
74
|
+
# @option options [Array<BrowsingContext>] :contexts Contexts to apply to
|
|
75
|
+
# @option options [String] :sandbox Sandbox name
|
|
76
|
+
# @return [String] Script ID
|
|
77
|
+
def add_preload_script(function_declaration, **options)
|
|
78
|
+
raise BrowserDisconnectedError, @reason if disconnected?
|
|
79
|
+
|
|
80
|
+
params = { functionDeclaration: function_declaration }
|
|
81
|
+
if options[:contexts]
|
|
82
|
+
params[:contexts] = options[:contexts].map(&:id)
|
|
83
|
+
end
|
|
84
|
+
params[:sandbox] = options[:sandbox] if options[:sandbox]
|
|
85
|
+
|
|
86
|
+
Async do
|
|
87
|
+
result = @session.async_send_command('script.addPreloadScript', params).wait
|
|
88
|
+
result['script']
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Remove a preload script
|
|
93
|
+
# @param script [String] Script ID
|
|
94
|
+
def remove_preload_script(script)
|
|
95
|
+
raise BrowserDisconnectedError, @reason if disconnected?
|
|
96
|
+
@session.async_send_command('script.removePreloadScript', { script: script })
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Create a new user context
|
|
100
|
+
# @param options [Hash] User context options
|
|
101
|
+
# @option options [Hash] :proxy Proxy configuration
|
|
102
|
+
# @return [UserContext] New user context
|
|
103
|
+
def create_user_context(**options)
|
|
104
|
+
raise BrowserDisconnectedError, @reason if disconnected?
|
|
105
|
+
|
|
106
|
+
params = {}
|
|
107
|
+
if options[:proxy_server]
|
|
108
|
+
params[:proxy] = {
|
|
109
|
+
proxyType: 'manual',
|
|
110
|
+
httpProxy: options[:proxy_server],
|
|
111
|
+
sslProxy: options[:proxy_server],
|
|
112
|
+
noProxy: options[:proxy_bypass_list]
|
|
113
|
+
}.compact
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
Async do
|
|
117
|
+
result = @session.async_send_command('browser.createUserContext', params).wait
|
|
118
|
+
user_context_id = result['userContext']
|
|
119
|
+
|
|
120
|
+
create_user_context_object(user_context_id)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Remove a network intercept
|
|
125
|
+
# @param intercept [String] Intercept ID
|
|
126
|
+
def remove_intercept(intercept)
|
|
127
|
+
raise BrowserDisconnectedError, @reason if disconnected?
|
|
128
|
+
@session.async_send_command('network.removeIntercept', { intercept: intercept })
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
protected
|
|
132
|
+
|
|
133
|
+
def perform_dispose
|
|
134
|
+
@reason ||= 'Browser was disconnected, probably because the session ended'
|
|
135
|
+
emit(:closed, { reason: @reason }) if @closed
|
|
136
|
+
emit(:disconnected, { reason: @reason })
|
|
137
|
+
@disposables.dispose
|
|
138
|
+
super
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def initialize_browser
|
|
144
|
+
Async do
|
|
145
|
+
# Listen for session end
|
|
146
|
+
@session.on(:ended) do |data|
|
|
147
|
+
dispose_browser(data[:reason])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Listen for shared worker creation
|
|
151
|
+
@session.on('script.realmCreated') do |info|
|
|
152
|
+
next unless info['type'] == 'shared-worker'
|
|
153
|
+
# Create SharedWorkerRealm when implemented
|
|
154
|
+
# @shared_workers[info['realm']] = SharedWorkerRealm.from(self, info['realm'], info['origin'])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Sync existing user contexts and browsing contexts
|
|
158
|
+
sync_user_contexts.wait
|
|
159
|
+
sync_browsing_contexts.wait
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def sync_user_contexts
|
|
164
|
+
Async do
|
|
165
|
+
result = @session.async_send_command('browser.getUserContexts', {}).wait
|
|
166
|
+
user_contexts = result['userContexts']
|
|
167
|
+
|
|
168
|
+
user_contexts.each do |context_info|
|
|
169
|
+
create_user_context_object(context_info['userContext'])
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def sync_browsing_contexts
|
|
175
|
+
Async do
|
|
176
|
+
# Get all browsing contexts
|
|
177
|
+
result = @session.async_send_command('browsingContext.getTree', {}).wait
|
|
178
|
+
contexts = result['contexts']
|
|
179
|
+
|
|
180
|
+
# Track context IDs for detecting created/destroyed contexts during sync
|
|
181
|
+
context_ids = []
|
|
182
|
+
|
|
183
|
+
# Setup temporary listener for context creation during sync
|
|
184
|
+
temp_listener = @session.on('browsingContext.contextCreated') do |info|
|
|
185
|
+
context_ids << info['context']
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Process all contexts (including nested ones)
|
|
189
|
+
process_contexts(contexts, context_ids)
|
|
190
|
+
|
|
191
|
+
# Remove temporary listener
|
|
192
|
+
# @session.off('browsingContext.contextCreated', &temp_listener)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def process_contexts(contexts, context_ids)
|
|
197
|
+
contexts.each do |info|
|
|
198
|
+
# Emit context created event if not already tracked
|
|
199
|
+
unless context_ids.include?(info['context'])
|
|
200
|
+
@session.emit('browsingContext.contextCreated', info)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Process children recursively
|
|
204
|
+
process_contexts(info['children'], context_ids) if info['children']
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def create_user_context_object(id)
|
|
209
|
+
return @user_contexts[id] if @user_contexts[id]
|
|
210
|
+
|
|
211
|
+
user_context = UserContext.create(self, id)
|
|
212
|
+
@user_contexts[id] = user_context
|
|
213
|
+
|
|
214
|
+
# Listen for user context closure
|
|
215
|
+
user_context.once(:closed) do
|
|
216
|
+
@user_contexts.delete(id)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
user_context
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def dispose_browser(reason, closed: false)
|
|
223
|
+
@closed = closed
|
|
224
|
+
@reason = reason
|
|
225
|
+
dispose
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|