puppeteer-ruby 0.52.0 → 0.53.0
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 +4 -4
- data/docs/api_coverage.md +16 -15
- data/lib/puppeteer/accessibility.rb +300 -0
- data/lib/puppeteer/browser.rb +107 -8
- data/lib/puppeteer/browser_connector.rb +42 -6
- data/lib/puppeteer/browser_context.rb +0 -1
- data/lib/puppeteer/cdp_session.rb +29 -8
- data/lib/puppeteer/chrome_target_manager.rb +145 -34
- data/lib/puppeteer/chrome_user_data_dir.rb +89 -0
- data/lib/puppeteer/connection.rb +19 -0
- data/lib/puppeteer/coverage.rb +5 -0
- data/lib/puppeteer/css_coverage.rb +4 -0
- data/lib/puppeteer/dialog.rb +5 -0
- data/lib/puppeteer/element_handle.rb +2 -3
- data/lib/puppeteer/emulation_manager.rb +70 -9
- data/lib/puppeteer/events.rb +2 -0
- data/lib/puppeteer/execution_context.rb +7 -0
- data/lib/puppeteer/extension.rb +20 -6
- data/lib/puppeteer/frame.rb +7 -3
- data/lib/puppeteer/frame_manager.rb +71 -26
- data/lib/puppeteer/http_response.rb +15 -1
- data/lib/puppeteer/isolated_world.rb +10 -4
- data/lib/puppeteer/js_coverage.rb +4 -0
- data/lib/puppeteer/keyboard.rb +6 -0
- data/lib/puppeteer/launcher/browser_options.rb +13 -1
- data/lib/puppeteer/launcher/chrome.rb +48 -4
- data/lib/puppeteer/lifecycle_watcher.rb +1 -6
- data/lib/puppeteer/locators.rb +54 -26
- data/lib/puppeteer/mouse.rb +12 -24
- data/lib/puppeteer/network_manager.rb +16 -4
- data/lib/puppeteer/page.rb +259 -33
- data/lib/puppeteer/puppeteer.rb +13 -7
- data/lib/puppeteer/screen_recorder.rb +269 -0
- data/lib/puppeteer/target.rb +7 -3
- data/lib/puppeteer/touch_screen.rb +6 -0
- data/lib/puppeteer/tracing.rb +4 -0
- data/lib/puppeteer/version.rb +2 -2
- data/lib/puppeteer/wait_task.rb +1 -2
- data/lib/puppeteer/web_mcp.rb +227 -0
- data/lib/puppeteer/web_worker.rb +135 -9
- data/lib/puppeteer.rb +11 -1
- data/puppeteer-ruby.gemspec +1 -0
- data/sig/_supplementary.rbs +33 -1
- data/sig/puppeteer/accessibility.rbs +73 -0
- data/sig/puppeteer/browser.rbs +27 -3
- data/sig/puppeteer/cdp_session.rbs +4 -0
- data/sig/puppeteer/chrome_user_data_dir.rbs +30 -0
- data/sig/puppeteer/dialog.rbs +3 -0
- data/sig/puppeteer/element_handle.rbs +1 -2
- data/sig/puppeteer/execution_context.rbs +5 -0
- data/sig/puppeteer/extension.rbs +4 -0
- data/sig/puppeteer/frame.rbs +4 -2
- data/sig/puppeteer/http_response.rbs +4 -0
- data/sig/puppeteer/keyboard.rbs +4 -0
- data/sig/puppeteer/locators.rbs +3 -4
- data/sig/puppeteer/mouse.rbs +5 -6
- data/sig/puppeteer/page.rbs +48 -4
- data/sig/puppeteer/puppeteer.rbs +5 -5
- data/sig/puppeteer/screen_recorder.rbs +48 -0
- data/sig/puppeteer/touch_screen.rbs +4 -0
- data/sig/puppeteer/web_mcp.rbs +112 -0
- data/sig/puppeteer/web_worker.rbs +39 -0
- metadata +24 -2
data/lib/puppeteer/locators.rb
CHANGED
|
@@ -197,22 +197,21 @@ class Puppeteer::Locator
|
|
|
197
197
|
|
|
198
198
|
# @rbs delay: Numeric? -- Delay between down and up (ms)
|
|
199
199
|
# @rbs button: String? -- Mouse button
|
|
200
|
-
# @rbs click_count: Integer? -- Deprecated click count
|
|
201
200
|
# @rbs count: Integer? -- Number of clicks
|
|
202
201
|
# @rbs offset: Hash[Symbol, Numeric]? -- Click offset
|
|
203
202
|
# @rbs return: void -- No return value
|
|
204
|
-
def click(delay: nil, button: nil,
|
|
203
|
+
def click(delay: nil, button: nil, count: nil, offset: nil)
|
|
205
204
|
perform_action('Locator.click',
|
|
206
205
|
conditions: [
|
|
207
206
|
method(:ensure_element_is_in_viewport_if_needed),
|
|
208
207
|
method(:wait_for_stable_bounding_box_if_needed),
|
|
209
208
|
method(:wait_for_enabled_if_needed),
|
|
210
209
|
]) do |handle, _options|
|
|
211
|
-
handle.click(delay: delay, button: button,
|
|
210
|
+
handle.click(delay: delay, button: button, count: count, offset: offset)
|
|
212
211
|
end
|
|
213
212
|
end
|
|
214
213
|
|
|
215
|
-
# @rbs value: String -- Value to fill
|
|
214
|
+
# @rbs value: String | bool -- Value to fill
|
|
216
215
|
# @rbs typing_threshold: Integer -- Minimum length to switch to direct assignment
|
|
217
216
|
# @rbs return: void -- No return value
|
|
218
217
|
def fill(value, typing_threshold: 100)
|
|
@@ -433,21 +432,28 @@ class Puppeteer::Locator
|
|
|
433
432
|
return 'typeable-input';
|
|
434
433
|
}
|
|
435
434
|
if (el instanceof HTMLInputElement) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
'
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
435
|
+
switch (el.type) {
|
|
436
|
+
case 'checkbox':
|
|
437
|
+
case 'radio':
|
|
438
|
+
return 'checkable-input';
|
|
439
|
+
case 'text':
|
|
440
|
+
case 'url':
|
|
441
|
+
case 'tel':
|
|
442
|
+
case 'search':
|
|
443
|
+
case 'password':
|
|
444
|
+
case 'number':
|
|
445
|
+
case 'email':
|
|
446
|
+
return 'typeable-input';
|
|
447
|
+
default:
|
|
448
|
+
return 'other-input';
|
|
449
449
|
}
|
|
450
|
-
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
switch (el.getAttribute('role')) {
|
|
453
|
+
case 'checkbox':
|
|
454
|
+
case 'radio':
|
|
455
|
+
case 'switch':
|
|
456
|
+
return 'checkable-input';
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
if (el.isContentEditable) {
|
|
@@ -459,26 +465,47 @@ class Puppeteer::Locator
|
|
|
459
465
|
JAVASCRIPT
|
|
460
466
|
|
|
461
467
|
case input_type
|
|
468
|
+
when 'checkable-input'
|
|
469
|
+
current_state = handle.evaluate(<<~JAVASCRIPT)
|
|
470
|
+
toggleEl => {
|
|
471
|
+
if (
|
|
472
|
+
toggleEl.indeterminate ||
|
|
473
|
+
toggleEl.getAttribute('aria-checked') === 'mixed'
|
|
474
|
+
) {
|
|
475
|
+
return 'mixed';
|
|
476
|
+
}
|
|
477
|
+
return (
|
|
478
|
+
toggleEl.checked ||
|
|
479
|
+
toggleEl.getAttribute('aria-checked') === 'true'
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
JAVASCRIPT
|
|
483
|
+
handle.click if current_state == 'mixed' || current_state != !!value
|
|
462
484
|
when 'select'
|
|
463
485
|
handle.select(value)
|
|
464
486
|
when 'contenteditable', 'typeable-input'
|
|
465
|
-
if value.length < typing_threshold
|
|
487
|
+
if value.is_a?(String) && value.length < typing_threshold
|
|
466
488
|
text_to_type = handle.evaluate(<<~JAVASCRIPT, value)
|
|
467
489
|
(input, newValue) => {
|
|
490
|
+
const valString = String(newValue);
|
|
468
491
|
const currentValue = input.isContentEditable
|
|
469
492
|
? input.innerText
|
|
470
493
|
: input.value;
|
|
471
494
|
|
|
495
|
+
if (currentValue === valString) {
|
|
496
|
+
return '';
|
|
497
|
+
}
|
|
498
|
+
|
|
472
499
|
if (
|
|
473
|
-
|
|
474
|
-
!
|
|
500
|
+
!valString.startsWith(currentValue) ||
|
|
501
|
+
!currentValue
|
|
475
502
|
) {
|
|
476
503
|
if (input.isContentEditable) {
|
|
477
504
|
input.innerText = '';
|
|
478
505
|
} else {
|
|
479
506
|
input.value = '';
|
|
480
507
|
}
|
|
481
|
-
return
|
|
508
|
+
return valString;
|
|
482
509
|
}
|
|
483
510
|
const originalValue = input.isContentEditable
|
|
484
511
|
? input.innerText
|
|
@@ -491,7 +518,7 @@ class Puppeteer::Locator
|
|
|
491
518
|
input.value = '';
|
|
492
519
|
input.value = originalValue;
|
|
493
520
|
}
|
|
494
|
-
return
|
|
521
|
+
return valString.substring(originalValue.length);
|
|
495
522
|
}
|
|
496
523
|
JAVASCRIPT
|
|
497
524
|
text_to_type = text_to_type.to_s
|
|
@@ -510,16 +537,17 @@ class Puppeteer::Locator
|
|
|
510
537
|
handle.focus
|
|
511
538
|
handle.evaluate(<<~JAVASCRIPT, value)
|
|
512
539
|
(input, newValue) => {
|
|
540
|
+
const valString = String(newValue);
|
|
513
541
|
const currentValue = input.isContentEditable
|
|
514
542
|
? input.innerText
|
|
515
543
|
: input.value;
|
|
516
|
-
if (currentValue ===
|
|
544
|
+
if (currentValue === valString) {
|
|
517
545
|
return;
|
|
518
546
|
}
|
|
519
547
|
if (input.isContentEditable) {
|
|
520
|
-
input.innerText =
|
|
548
|
+
input.innerText = valString;
|
|
521
549
|
} else {
|
|
522
|
-
input.value =
|
|
550
|
+
input.value = valString;
|
|
523
551
|
}
|
|
524
552
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
525
553
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
data/lib/puppeteer/mouse.rb
CHANGED
|
@@ -40,6 +40,12 @@ class Puppeteer::Mouse
|
|
|
40
40
|
@dispatch_mutex = Mutex.new
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# @rbs client: Puppeteer::CDPSession -- Replacement CDP session
|
|
44
|
+
# @rbs return: void -- No return value
|
|
45
|
+
def update_client(client)
|
|
46
|
+
@client = client
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
# @rbs return: void -- No return value
|
|
44
50
|
def reset
|
|
45
51
|
[
|
|
@@ -102,48 +108,30 @@ class Puppeteer::Mouse
|
|
|
102
108
|
# @rbs y: Numeric -- Y coordinate
|
|
103
109
|
# @rbs delay: Numeric? -- Delay between down and up (ms)
|
|
104
110
|
# @rbs button: String? -- Mouse button
|
|
105
|
-
# @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
|
|
106
111
|
# @rbs count: Integer? -- Number of click repetitions
|
|
107
112
|
# @rbs return: void -- No return value
|
|
108
|
-
def click(x, y, delay: nil, button: nil,
|
|
109
|
-
warn_deprecated_click_count if !click_count.nil?
|
|
113
|
+
def click(x, y, delay: nil, button: nil, count: nil)
|
|
110
114
|
count ||= 1
|
|
111
|
-
click_count ||= count
|
|
112
115
|
if count < 1
|
|
113
116
|
raise Puppeteer::Error.new('Click must occur a positive number of times.')
|
|
114
117
|
end
|
|
115
118
|
# Serialize click sequences to keep event ordering stable under thread-based concurrency.
|
|
116
119
|
@dispatch_mutex.synchronize do
|
|
117
120
|
move(x, y)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
up(button: button, click_count: i)
|
|
122
|
-
end
|
|
121
|
+
1.upto(count - 1) do |i|
|
|
122
|
+
down(button: button, click_count: i)
|
|
123
|
+
up(button: button, click_count: i)
|
|
123
124
|
end
|
|
124
|
-
down(button: button, click_count:
|
|
125
|
+
down(button: button, click_count: count)
|
|
125
126
|
if !delay.nil?
|
|
126
127
|
Puppeteer::AsyncUtils.sleep_seconds(delay / 1000.0)
|
|
127
128
|
end
|
|
128
|
-
up(button: button, click_count:
|
|
129
|
+
up(button: button, click_count: count)
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
define_async_method :async_click
|
|
133
134
|
|
|
134
|
-
private def warn_deprecated_click_count
|
|
135
|
-
return if self.class.deprecated_click_count_warned
|
|
136
|
-
|
|
137
|
-
self.class.deprecated_click_count_warned = true
|
|
138
|
-
warn('DEPRECATED: `click_count` is deprecated; use `count` instead.')
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
class << self
|
|
142
|
-
attr_accessor :deprecated_click_count_warned
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
self.deprecated_click_count_warned = false
|
|
146
|
-
|
|
147
135
|
# @rbs button: String? -- Mouse button
|
|
148
136
|
# @rbs click_count: Integer? -- Click count to report
|
|
149
137
|
# @rbs return: void -- No return value
|
|
@@ -99,6 +99,7 @@ class Puppeteer::NetworkManager
|
|
|
99
99
|
@extra_http_headers = {}
|
|
100
100
|
@user_agent = nil
|
|
101
101
|
@user_agent_metadata = nil
|
|
102
|
+
@accept_language = nil
|
|
102
103
|
|
|
103
104
|
@attempted_authentications = Set.new
|
|
104
105
|
@user_request_interception_enabled = false
|
|
@@ -146,12 +147,12 @@ class Puppeteer::NetworkManager
|
|
|
146
147
|
end
|
|
147
148
|
|
|
148
149
|
private def ignore_client_error?(error)
|
|
150
|
+
return true if error.is_a?(Puppeteer::TargetCloseError)
|
|
151
|
+
|
|
149
152
|
message = error&.message
|
|
150
153
|
return false unless message
|
|
151
154
|
|
|
152
155
|
lowered = message.downcase
|
|
153
|
-
return true if lowered.include?('target closed')
|
|
154
|
-
return true if lowered.include?('session closed')
|
|
155
156
|
return true if lowered.include?('not supported')
|
|
156
157
|
return true if lowered.include?("wasn't found")
|
|
157
158
|
|
|
@@ -175,10 +176,12 @@ class Puppeteer::NetworkManager
|
|
|
175
176
|
end
|
|
176
177
|
|
|
177
178
|
private def apply_user_agent(client)
|
|
178
|
-
|
|
179
|
+
user_agent = @user_agent || @frame_manager.page.browser.user_agent
|
|
180
|
+
return unless user_agent
|
|
179
181
|
|
|
180
182
|
safe_send_message(client, 'Network.setUserAgentOverride', {
|
|
181
|
-
userAgent:
|
|
183
|
+
userAgent: user_agent,
|
|
184
|
+
acceptLanguage: @accept_language,
|
|
182
185
|
userAgentMetadata: @user_agent_metadata,
|
|
183
186
|
}.compact)
|
|
184
187
|
end
|
|
@@ -212,6 +215,8 @@ class Puppeteer::NetworkManager
|
|
|
212
215
|
if @internal_network_condition.active?
|
|
213
216
|
safe_send_message(client, 'Network.emulateNetworkConditions', @internal_network_condition.params)
|
|
214
217
|
end
|
|
218
|
+
rescue => err
|
|
219
|
+
raise unless ignore_client_error?(err)
|
|
215
220
|
end
|
|
216
221
|
|
|
217
222
|
# @param username [String|NilClass]
|
|
@@ -281,6 +286,13 @@ class Puppeteer::NetworkManager
|
|
|
281
286
|
end
|
|
282
287
|
alias_method :user_agent=, :set_user_agent
|
|
283
288
|
|
|
289
|
+
# @rbs accept_language: String? -- Accept-Language override
|
|
290
|
+
# @rbs return: void -- No return value
|
|
291
|
+
def set_accept_language(accept_language)
|
|
292
|
+
@accept_language = accept_language
|
|
293
|
+
apply_to_clients { |client| apply_user_agent(client) }
|
|
294
|
+
end
|
|
295
|
+
|
|
284
296
|
def cache_enabled=(enabled)
|
|
285
297
|
@user_cache_disabled = !enabled
|
|
286
298
|
update_protocol_cache_disabled
|
data/lib/puppeteer/page.rb
CHANGED
|
@@ -40,32 +40,46 @@ class Puppeteer::Page
|
|
|
40
40
|
@closed = false
|
|
41
41
|
@client = client
|
|
42
42
|
@target = target
|
|
43
|
-
@
|
|
43
|
+
@tab_session = client.parent_session || client
|
|
44
|
+
@tab_target = @tab_session.target || target
|
|
45
|
+
@tab_id = @tab_target.target_id
|
|
44
46
|
@keyboard = Puppeteer::Keyboard.new(client)
|
|
45
47
|
@mouse = Puppeteer::Mouse.new(client, @keyboard)
|
|
46
48
|
@timeout_settings = Puppeteer::TimeoutSettings.new
|
|
47
49
|
@touchscreen = Puppeteer::TouchScreen.new(client, @keyboard)
|
|
48
|
-
# @accessibility = Accessibility.new(client)
|
|
49
50
|
@frame_manager = Puppeteer::FrameManager.new(client, self, ignore_https_errors, @timeout_settings, network_enabled: network_enabled)
|
|
50
51
|
@emulation_manager = Puppeteer::EmulationManager.new(client)
|
|
51
52
|
@tracing = Puppeteer::Tracing.new(client)
|
|
53
|
+
@webmcp = Puppeteer::WebMCP.new(client, @frame_manager)
|
|
52
54
|
@page_bindings = {}
|
|
53
55
|
@page_binding_ids = {}
|
|
54
56
|
@coverage = Puppeteer::Coverage.new(client)
|
|
55
57
|
@javascript_enabled = true
|
|
56
58
|
@screenshot_task_queue = ScreenshotTaskQueue.new
|
|
59
|
+
@screencast_session_count = 0
|
|
60
|
+
@screencast_start_promise = nil
|
|
61
|
+
@screencast_mutex = Mutex.new
|
|
57
62
|
@inflight_requests = Set.new
|
|
58
63
|
@request_intercepted_listener_map = ObjectSpace::WeakMap.new
|
|
59
64
|
@attached_sessions = Set.new
|
|
65
|
+
@primary_session_listener_ids = []
|
|
60
66
|
|
|
61
67
|
@workers = {}
|
|
62
68
|
@user_drag_interception_enabled = false
|
|
63
69
|
@service_worker_bypassed = false
|
|
64
70
|
|
|
65
|
-
@
|
|
66
|
-
|
|
71
|
+
@swapped_session_listener_id = @tab_session.add_event_listener(CDPSessionEmittedEvents::Swapped) do |session|
|
|
72
|
+
Async do
|
|
73
|
+
handle_activation(session)
|
|
74
|
+
rescue => err
|
|
75
|
+
debug_puts(err)
|
|
76
|
+
end
|
|
67
77
|
end
|
|
68
|
-
@
|
|
78
|
+
@secondary_session_listener_id = @tab_session.add_event_listener(CDPSessionEmittedEvents::Ready) do |session|
|
|
79
|
+
handle_secondary_target(session)
|
|
80
|
+
end
|
|
81
|
+
@target_manager = @target.target_manager
|
|
82
|
+
@target_gone_listener_id = @target_manager.add_event_listener(
|
|
69
83
|
TargetManagerEmittedEvents::TargetGone,
|
|
70
84
|
&method(:handle_detached_from_target)
|
|
71
85
|
)
|
|
@@ -102,47 +116,95 @@ class Puppeteer::Page
|
|
|
102
116
|
@file_chooser_interception_is_disabled = false
|
|
103
117
|
@file_chooser_interceptors = Set.new
|
|
104
118
|
|
|
105
|
-
@client
|
|
119
|
+
setup_primary_target_listeners(@client)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private def setup_primary_target_listeners(client)
|
|
123
|
+
listener_id = client.add_event_listener(CDPSessionEmittedEvents::Ready) do |session|
|
|
124
|
+
handle_attached_to_session(session)
|
|
125
|
+
end
|
|
126
|
+
@primary_session_listener_ids << [client, listener_id]
|
|
127
|
+
|
|
128
|
+
client.on_event('Page.domContentEventFired') do |event|
|
|
106
129
|
emit_event(PageEmittedEvents::DOMContentLoaded)
|
|
107
130
|
end
|
|
108
|
-
|
|
131
|
+
client.on_event('Page.loadEventFired') do |event|
|
|
109
132
|
emit_event(PageEmittedEvents::Load)
|
|
110
133
|
end
|
|
111
|
-
|
|
134
|
+
client.add_event_listener('Runtime.consoleAPICalled') do |event|
|
|
112
135
|
handle_console_api(event)
|
|
113
136
|
end
|
|
114
|
-
|
|
137
|
+
client.add_event_listener('Runtime.bindingCalled') do |event|
|
|
115
138
|
handle_binding_called(event)
|
|
116
139
|
end
|
|
117
|
-
|
|
140
|
+
client.on_event('Page.javascriptDialogOpening') do |event|
|
|
118
141
|
handle_dialog_opening(event)
|
|
119
142
|
end
|
|
120
|
-
|
|
143
|
+
client.on_event('Runtime.exceptionThrown') do |exception|
|
|
121
144
|
handle_exception(exception['exceptionDetails'])
|
|
122
145
|
end
|
|
123
|
-
|
|
146
|
+
client.on_event('Inspector.targetCrashed') do |event|
|
|
124
147
|
handle_target_crashed
|
|
125
148
|
end
|
|
126
|
-
|
|
149
|
+
client.on_event('Performance.metrics') do |event|
|
|
127
150
|
emit_event(PageEmittedEvents::Metrics, MetricsEvent.new(event))
|
|
128
151
|
end
|
|
129
|
-
|
|
152
|
+
client.on_event('Log.entryAdded') do |event|
|
|
130
153
|
handle_log_entry_added(event)
|
|
131
154
|
end
|
|
132
|
-
|
|
155
|
+
client.on_event('Page.fileChooserOpened') do |event|
|
|
133
156
|
handle_file_chooser(event)
|
|
134
157
|
end
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private def handle_activation(session)
|
|
161
|
+
@client = session
|
|
162
|
+
@target = session.target
|
|
163
|
+
@target.instance_variable_set(:@page, self)
|
|
164
|
+
@keyboard.update_client(session)
|
|
165
|
+
@mouse.update_client(session)
|
|
166
|
+
@touchscreen.update_client(session)
|
|
167
|
+
@emulation_manager.update_client(session)
|
|
168
|
+
@tracing.update_client(session)
|
|
169
|
+
@webmcp.update_client(session)
|
|
170
|
+
@coverage.update_client(session)
|
|
171
|
+
@frame_manager.swap_frame_tree(session)
|
|
172
|
+
setup_primary_target_listeners(session)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private def handle_secondary_target(session)
|
|
176
|
+
target = session.target
|
|
177
|
+
return unless target&.target_info&.subtype == 'prerender'
|
|
139
178
|
|
|
140
|
-
|
|
141
|
-
|
|
179
|
+
@frame_manager.register_speculative_session(session)
|
|
180
|
+
@emulation_manager.register_speculative_session(session)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private def mark_closed
|
|
184
|
+
return if @closed
|
|
185
|
+
|
|
186
|
+
cleanup_page_listeners
|
|
187
|
+
@closed = true
|
|
188
|
+
emit_event(PageEmittedEvents::Close)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private def cleanup_page_listeners
|
|
192
|
+
return if @page_listeners_cleaned
|
|
193
|
+
|
|
194
|
+
@page_listeners_cleaned = true
|
|
195
|
+
@primary_session_listener_ids.each do |session, listener_id|
|
|
196
|
+
session.remove_event_listener(listener_id)
|
|
142
197
|
end
|
|
198
|
+
@tab_session.remove_event_listener(@swapped_session_listener_id, @secondary_session_listener_id)
|
|
199
|
+
@target_manager.remove_event_listener(@target_gone_listener_id)
|
|
143
200
|
end
|
|
144
201
|
|
|
145
202
|
private def handle_detached_from_target(target)
|
|
203
|
+
if target == @tab_target
|
|
204
|
+
mark_closed
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
146
208
|
session_id = target.session&.id
|
|
147
209
|
@frame_manager.handle_detached_from_target(target)
|
|
148
210
|
return unless session_id
|
|
@@ -201,6 +263,7 @@ class Puppeteer::Page
|
|
|
201
263
|
@frame_manager.async_init(@target.target_id),
|
|
202
264
|
@client.async_send_message('Performance.enable'),
|
|
203
265
|
@client.async_send_message('Log.enable'),
|
|
266
|
+
@webmcp.async_initialize_domain,
|
|
204
267
|
)
|
|
205
268
|
end
|
|
206
269
|
|
|
@@ -360,10 +423,23 @@ class Puppeteer::Page
|
|
|
360
423
|
end
|
|
361
424
|
|
|
362
425
|
# @rbs return: bool -- Whether DevTools is attached to this page
|
|
363
|
-
def
|
|
426
|
+
def has_dev_tools
|
|
364
427
|
!!browser._has_devtools_target(@target.target_id)
|
|
365
428
|
end
|
|
366
429
|
|
|
430
|
+
# @rbs return: Puppeteer::Page -- Existing or newly opened DevTools page
|
|
431
|
+
def open_dev_tools
|
|
432
|
+
page_target_id = @target.target_id
|
|
433
|
+
devtools_target_id = browser._has_devtools_target(page_target_id)
|
|
434
|
+
if devtools_target_id
|
|
435
|
+
return browser._get_devtools_target_page(devtools_target_id)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
browser._create_devtools_page(page_target_id)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
define_async_method :async_open_dev_tools
|
|
442
|
+
|
|
367
443
|
# @rbs extension: Puppeteer::Extension -- Extension to trigger
|
|
368
444
|
# @rbs return: void -- No return value
|
|
369
445
|
def trigger_extension_action(extension)
|
|
@@ -409,9 +485,14 @@ class Puppeteer::Page
|
|
|
409
485
|
@frame_manager.main_frame
|
|
410
486
|
end
|
|
411
487
|
|
|
412
|
-
attr_reader :touchscreen, :coverage, :tracing, :
|
|
488
|
+
attr_reader :touchscreen, :coverage, :tracing, :webmcp
|
|
413
489
|
alias_method :touch_screen, :touchscreen
|
|
414
490
|
|
|
491
|
+
# @rbs return: Puppeteer::Accessibility -- Main-frame accessibility tree
|
|
492
|
+
def accessibility
|
|
493
|
+
main_frame.accessibility
|
|
494
|
+
end
|
|
495
|
+
|
|
415
496
|
# @rbs block: Proc? -- Optional block for instance_eval
|
|
416
497
|
# @rbs return: Puppeteer::Keyboard -- Keyboard instance
|
|
417
498
|
def keyboard(&block)
|
|
@@ -1324,6 +1405,13 @@ class Puppeteer::Page
|
|
|
1324
1405
|
end
|
|
1325
1406
|
end
|
|
1326
1407
|
|
|
1408
|
+
# @rbs locale: String? -- Locale to emulate, or nil to disable emulation
|
|
1409
|
+
# @rbs return: void -- No return value
|
|
1410
|
+
def emulate_locale(locale = nil)
|
|
1411
|
+
@emulation_manager.emulate_locale(locale)
|
|
1412
|
+
@frame_manager.network_manager.set_accept_language(locale)
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1327
1415
|
VISION_DEFICIENCY_TYPES = %w[
|
|
1328
1416
|
none
|
|
1329
1417
|
achromatopsia
|
|
@@ -1369,6 +1457,151 @@ class Puppeteer::Page
|
|
|
1369
1457
|
|
|
1370
1458
|
attr_reader :viewport
|
|
1371
1459
|
|
|
1460
|
+
# @rbs path: String? -- Output file path
|
|
1461
|
+
# @rbs overwrite: bool -- Overwrite an existing output file
|
|
1462
|
+
# @rbs format: String? -- webm, gif, or mp4
|
|
1463
|
+
# @rbs crop: Hash[Symbol, Numeric]? -- Viewport crop rectangle
|
|
1464
|
+
# @rbs scale: Numeric? -- Output scale
|
|
1465
|
+
# @rbs speed: Numeric? -- Playback speed
|
|
1466
|
+
# @rbs fps: Numeric? -- Output frame rate
|
|
1467
|
+
# @rbs loop: Numeric? -- GIF loop count
|
|
1468
|
+
# @rbs delay: Numeric? -- GIF loop delay
|
|
1469
|
+
# @rbs quality: Numeric? -- VP9 CRF value
|
|
1470
|
+
# @rbs colors: Numeric? -- GIF palette size
|
|
1471
|
+
# @rbs ffmpeg_path: String? -- FFmpeg executable path
|
|
1472
|
+
# @rbs return: Puppeteer::ScreenRecorder -- Active recorder
|
|
1473
|
+
def screencast(
|
|
1474
|
+
path: nil,
|
|
1475
|
+
overwrite: true,
|
|
1476
|
+
format: nil,
|
|
1477
|
+
crop: nil,
|
|
1478
|
+
scale: nil,
|
|
1479
|
+
speed: nil,
|
|
1480
|
+
fps: nil,
|
|
1481
|
+
loop: nil,
|
|
1482
|
+
delay: nil,
|
|
1483
|
+
quality: nil,
|
|
1484
|
+
colors: nil,
|
|
1485
|
+
ffmpeg_path: nil
|
|
1486
|
+
)
|
|
1487
|
+
raise ArgumentError.new('`speed` must be greater than 0.') if speed && speed <= 0
|
|
1488
|
+
raise ArgumentError.new('`scale` must be greater than 0.') if scale && scale <= 0
|
|
1489
|
+
width, height, device_pixel_ratio = native_pixel_dimensions
|
|
1490
|
+
normalized_crop = normalize_screencast_crop(crop, width, height, device_pixel_ratio)
|
|
1491
|
+
options = {
|
|
1492
|
+
path: path,
|
|
1493
|
+
overwrite: overwrite,
|
|
1494
|
+
format: format,
|
|
1495
|
+
crop: normalized_crop,
|
|
1496
|
+
scale: scale,
|
|
1497
|
+
speed: speed,
|
|
1498
|
+
fps: fps,
|
|
1499
|
+
loop: loop,
|
|
1500
|
+
delay: delay,
|
|
1501
|
+
quality: quality,
|
|
1502
|
+
colors: colors,
|
|
1503
|
+
ffmpeg_path: ffmpeg_path,
|
|
1504
|
+
}.compact
|
|
1505
|
+
recorder = Puppeteer::ScreenRecorder.new(self, width, height, options)
|
|
1506
|
+
begin
|
|
1507
|
+
_start_screencast
|
|
1508
|
+
rescue
|
|
1509
|
+
recorder.stop
|
|
1510
|
+
raise
|
|
1511
|
+
end
|
|
1512
|
+
recorder
|
|
1513
|
+
end
|
|
1514
|
+
|
|
1515
|
+
# @rbs return: void -- Start a shared CDP screencast session
|
|
1516
|
+
def _start_screencast
|
|
1517
|
+
start_promise, first_session = @screencast_mutex.synchronize do
|
|
1518
|
+
@screencast_session_count += 1
|
|
1519
|
+
next [@screencast_start_promise, false] if @screencast_start_promise
|
|
1520
|
+
|
|
1521
|
+
@screencast_start_promise = Async::Promise.new
|
|
1522
|
+
[@screencast_start_promise, true]
|
|
1523
|
+
end
|
|
1524
|
+
if first_session
|
|
1525
|
+
first_frame = Async::Promise.new
|
|
1526
|
+
client = main_frame.client
|
|
1527
|
+
listener_id = client.once('Page.screencastFrame') do
|
|
1528
|
+
first_frame.resolve(nil) unless first_frame.resolved?
|
|
1529
|
+
end
|
|
1530
|
+
begin
|
|
1531
|
+
client.send_message('Page.startScreencast', format: 'png')
|
|
1532
|
+
first_frame.wait
|
|
1533
|
+
start_promise.resolve(nil)
|
|
1534
|
+
rescue => error
|
|
1535
|
+
start_promise.reject(error) unless start_promise.resolved?
|
|
1536
|
+
ensure
|
|
1537
|
+
client.remove_event_listener(listener_id) if listener_id
|
|
1538
|
+
end
|
|
1539
|
+
end
|
|
1540
|
+
start_promise.wait
|
|
1541
|
+
end
|
|
1542
|
+
|
|
1543
|
+
# @rbs return: void -- Stop a shared CDP screencast session
|
|
1544
|
+
def _stop_screencast
|
|
1545
|
+
should_stop = @screencast_mutex.synchronize do
|
|
1546
|
+
@screencast_session_count -= 1 if @screencast_session_count.positive?
|
|
1547
|
+
next false unless @screencast_start_promise
|
|
1548
|
+
|
|
1549
|
+
@screencast_start_promise = nil
|
|
1550
|
+
@screencast_session_count.zero?
|
|
1551
|
+
end
|
|
1552
|
+
main_frame.client.send_message('Page.stopScreencast') if should_stop
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
private def native_pixel_dimensions
|
|
1556
|
+
original_viewport = @viewport
|
|
1557
|
+
if original_viewport && original_viewport.device_scale_factor != 0
|
|
1558
|
+
self.viewport = original_viewport.merge(device_scale_factor: 0)
|
|
1559
|
+
end
|
|
1560
|
+
main_frame.puppeteer_world.evaluate(<<~JAVASCRIPT)
|
|
1561
|
+
() => [
|
|
1562
|
+
window.visualViewport.width * window.devicePixelRatio,
|
|
1563
|
+
window.visualViewport.height * window.devicePixelRatio,
|
|
1564
|
+
window.devicePixelRatio,
|
|
1565
|
+
]
|
|
1566
|
+
JAVASCRIPT
|
|
1567
|
+
ensure
|
|
1568
|
+
self.viewport = original_viewport if original_viewport && @viewport != original_viewport
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
private def normalize_screencast_crop(crop, width, height, device_pixel_ratio)
|
|
1572
|
+
return nil unless crop
|
|
1573
|
+
|
|
1574
|
+
value = ->(name) do
|
|
1575
|
+
crop.respond_to?(name) ? crop.public_send(name) : crop[name] || crop[name.to_s]
|
|
1576
|
+
end
|
|
1577
|
+
x = value.call(:x).floor
|
|
1578
|
+
y = value.call(:y).floor
|
|
1579
|
+
crop_width = value.call(:width).ceil
|
|
1580
|
+
crop_height = value.call(:height).ceil
|
|
1581
|
+
if x.negative? || y.negative?
|
|
1582
|
+
raise ArgumentError.new('`crop.x` and `crop.y` must be greater than or equal to 0.')
|
|
1583
|
+
end
|
|
1584
|
+
if crop_width <= 0 || crop_height <= 0
|
|
1585
|
+
raise ArgumentError.new(
|
|
1586
|
+
'`crop.height` and `crop.width` must be greater than or equal to 0.',
|
|
1587
|
+
)
|
|
1588
|
+
end
|
|
1589
|
+
viewport_width = width / device_pixel_ratio
|
|
1590
|
+
viewport_height = height / device_pixel_ratio
|
|
1591
|
+
if x + crop_width > viewport_width
|
|
1592
|
+
raise ArgumentError.new("`crop.width` cannot be larger than the viewport width (#{viewport_width}).")
|
|
1593
|
+
end
|
|
1594
|
+
if y + crop_height > viewport_height
|
|
1595
|
+
raise ArgumentError.new("`crop.height` cannot be larger than the viewport height (#{viewport_height}).")
|
|
1596
|
+
end
|
|
1597
|
+
{
|
|
1598
|
+
x: x * device_pixel_ratio,
|
|
1599
|
+
y: y * device_pixel_ratio,
|
|
1600
|
+
width: crop_width * device_pixel_ratio,
|
|
1601
|
+
height: crop_height * device_pixel_ratio,
|
|
1602
|
+
}
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1372
1605
|
# @rbs page_function: String -- page_function parameter
|
|
1373
1606
|
# @rbs args: Array[untyped] -- args parameter
|
|
1374
1607
|
# @rbs return: untyped -- Result
|
|
@@ -1619,13 +1852,7 @@ class Puppeteer::Page
|
|
|
1619
1852
|
@client.send_message('Page.close')
|
|
1620
1853
|
else
|
|
1621
1854
|
@client.connection.send_message('Target.closeTarget', targetId: @target.target_id)
|
|
1622
|
-
@
|
|
1623
|
-
|
|
1624
|
-
# @closed sometimes remains false, so wait for @closed = true with 100ms timeout.
|
|
1625
|
-
25.times do
|
|
1626
|
-
break if @closed
|
|
1627
|
-
Puppeteer::AsyncUtils.sleep_seconds(0.004)
|
|
1628
|
-
end
|
|
1855
|
+
@tab_target.is_closed_promise.wait
|
|
1629
1856
|
end
|
|
1630
1857
|
rescue Puppeteer::Connection::ProtocolError => err
|
|
1631
1858
|
raise unless err.message.match?(/Target closed/i)
|
|
@@ -1644,11 +1871,10 @@ class Puppeteer::Page
|
|
|
1644
1871
|
# @rbs selector: String -- CSS selector
|
|
1645
1872
|
# @rbs delay: Numeric? -- Delay between down and up (ms)
|
|
1646
1873
|
# @rbs button: String? -- Mouse button
|
|
1647
|
-
# @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
|
|
1648
1874
|
# @rbs count: Integer? -- Number of clicks to perform
|
|
1649
1875
|
# @rbs return: void -- No return value
|
|
1650
|
-
def click(selector, delay: nil, button: nil,
|
|
1651
|
-
main_frame.click(selector, delay: delay, button: button,
|
|
1876
|
+
def click(selector, delay: nil, button: nil, count: nil)
|
|
1877
|
+
main_frame.click(selector, delay: delay, button: button, count: count)
|
|
1652
1878
|
end
|
|
1653
1879
|
|
|
1654
1880
|
define_async_method :async_click
|