puppeteer-ruby 0.50.1 → 0.52.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/docs/api_coverage.md +38 -6
  3. data/lib/puppeteer/browser.rb +82 -0
  4. data/lib/puppeteer/browser_connector.rb +2 -0
  5. data/lib/puppeteer/browser_context.rb +51 -3
  6. data/lib/puppeteer/chrome_target_manager.rb +53 -6
  7. data/lib/puppeteer/debug_print.rb +9 -13
  8. data/lib/puppeteer/element_handle.rb +16 -0
  9. data/lib/puppeteer/events.rb +3 -0
  10. data/lib/puppeteer/execution_context.rb +37 -15
  11. data/lib/puppeteer/extension.rb +70 -0
  12. data/lib/puppeteer/frame.rb +8 -1
  13. data/lib/puppeteer/frame_manager.rb +62 -2
  14. data/lib/puppeteer/http_request.rb +15 -1
  15. data/lib/puppeteer/http_response.rb +6 -1
  16. data/lib/puppeteer/isolated_world.rb +22 -1
  17. data/lib/puppeteer/issue.rb +16 -0
  18. data/lib/puppeteer/js_coverage.rb +14 -1
  19. data/lib/puppeteer/launcher/browser_options.rb +6 -1
  20. data/lib/puppeteer/launcher/chrome.rb +17 -2
  21. data/lib/puppeteer/launcher/chrome_arg_options.rb +2 -1
  22. data/lib/puppeteer/locators.rb +56 -37
  23. data/lib/puppeteer/network_manager.rb +16 -16
  24. data/lib/puppeteer/page.rb +90 -7
  25. data/lib/puppeteer/puppeteer.rb +15 -0
  26. data/lib/puppeteer/remote_object.rb +2 -1
  27. data/lib/puppeteer/target.rb +17 -0
  28. data/lib/puppeteer/version.rb +2 -1
  29. data/lib/puppeteer.rb +2 -0
  30. data/puppeteer-ruby.gemspec +1 -1
  31. data/sig/_supplementary.rbs +7 -0
  32. data/sig/puppeteer/browser.rbs +34 -2
  33. data/sig/puppeteer/element_handle.rbs +5 -0
  34. data/sig/puppeteer/execution_context.rbs +4 -0
  35. data/sig/puppeteer/extension.rbs +37 -0
  36. data/sig/puppeteer/frame.rbs +5 -0
  37. data/sig/puppeteer/http_request.rbs +2 -0
  38. data/sig/puppeteer/issue.rbs +13 -0
  39. data/sig/puppeteer/locators.rbs +5 -2
  40. data/sig/puppeteer/page.rbs +20 -0
  41. data/sig/puppeteer/puppeteer.rbs +7 -2
  42. data/sig/puppeteer/remote_object.rbs +2 -0
  43. metadata +8 -4
@@ -0,0 +1,70 @@
1
+ # rbs_inline: enabled
2
+
3
+ class Puppeteer::Extension
4
+ # @rbs id: String -- Extension id
5
+ # @rbs version: String -- Extension version
6
+ # @rbs name: String -- Extension name
7
+ # @rbs path: String -- Extension path
8
+ # @rbs enabled: bool -- Whether extension is enabled
9
+ # @rbs browser: Puppeteer::Browser -- Browser instance
10
+ # @rbs return: void -- No return value
11
+ def initialize(id:, version:, name:, path:, enabled:, browser:)
12
+ @id = id
13
+ @version = version
14
+ @name = name
15
+ @path = path
16
+ @enabled = enabled
17
+ @browser = browser
18
+ end
19
+
20
+ # @rbs return: String -- Extension id
21
+ attr_reader :id
22
+
23
+ # @rbs return: String -- Extension version
24
+ attr_reader :version
25
+
26
+ # @rbs return: String -- Extension name
27
+ attr_reader :name
28
+
29
+ # @rbs return: String -- Extension path
30
+ attr_reader :path
31
+
32
+ # @rbs return: bool -- Whether extension is enabled
33
+ attr_reader :enabled
34
+
35
+ # @rbs return: Array[Puppeteer::CdpWebWorker] -- Extension workers
36
+ def workers
37
+ extension_prefix = "chrome-extension://#{@id}"
38
+ extension_targets = @browser.targets.select do |target|
39
+ target.type == 'service_worker' && target.url.start_with?(extension_prefix)
40
+ end
41
+ extension_targets.filter_map do |target|
42
+ target.worker
43
+ rescue
44
+ nil
45
+ end
46
+ end
47
+
48
+ # @rbs return: Array[Puppeteer::Page] -- Extension pages
49
+ def pages
50
+ extension_prefix = "chrome-extension://#{@id}"
51
+ extension_targets = @browser.targets.select do |target|
52
+ target_url = target.url
53
+ ['page', 'background_page'].include?(target.type) && target_url.start_with?(extension_prefix)
54
+ end
55
+ extension_targets.filter_map do |target|
56
+ target.as_page
57
+ rescue
58
+ nil
59
+ end
60
+ end
61
+
62
+ # @rbs page: Puppeteer::Page -- Target page
63
+ # @rbs return: void -- No return value
64
+ def trigger_action(page)
65
+ page.browser.send(:connection).send_message('Extensions.triggerAction', {
66
+ id: @id,
67
+ targetId: page._tab_id,
68
+ })
69
+ end
70
+ end
@@ -19,6 +19,7 @@ class Puppeteer::Frame
19
19
  @url = 'about:blank'
20
20
  @lifecycle_events = Set.new
21
21
  @child_frames = Set.new
22
+ @extension_worlds = {}
22
23
  if parent_frame
23
24
  parent_frame._child_frames << self
24
25
  end
@@ -71,7 +72,7 @@ class Puppeteer::Frame
71
72
  @client != @frame_manager.client
72
73
  end
73
74
 
74
- attr_accessor :frame_manager, :id, :loader_id, :lifecycle_events, :main_world, :puppeteer_world
75
+ attr_accessor :frame_manager, :id, :loader_id, :lifecycle_events, :main_world, :puppeteer_world, :extension_worlds
75
76
  attr_reader :client
76
77
 
77
78
  # @rbs other: Object -- Other object to compare
@@ -229,6 +230,11 @@ class Puppeteer::Frame
229
230
  @url
230
231
  end
231
232
 
233
+ # @rbs return: Array[untyped] -- Extension execution realms for this frame
234
+ def extension_realms
235
+ @extension_worlds.values
236
+ end
237
+
232
238
  # @rbs return: Puppeteer::Frame? -- Parent frame
233
239
  def parent_frame
234
240
  @parent_frame
@@ -425,6 +431,7 @@ class Puppeteer::Frame
425
431
  @detached = true
426
432
  @main_world.detach
427
433
  @puppeteer_world.detach
434
+ @extension_worlds.each_value(&:detach)
428
435
  if @parent_frame
429
436
  @parent_frame._child_frames.delete(self)
430
437
  end
@@ -5,6 +5,7 @@ class Puppeteer::FrameManager
5
5
  using Puppeteer::DefineAsyncMethod
6
6
 
7
7
  UTILITY_WORLD_NAME = '__puppeteer_utility_world__'
8
+ CHROME_EXTENSION_PREFIX = 'chrome-extension://'
8
9
 
9
10
  # @param {!Puppeteer.CDPSession} client
10
11
  # @param {!Puppeteer.Page} page
@@ -89,6 +90,9 @@ class Puppeteer::FrameManager
89
90
  handle_lifecycle_event(event)
90
91
  end
91
92
  end
93
+ client.on_event('Audits.issueAdded') do |event|
94
+ @page.emit_event(PageEmittedEvents::Issue, Puppeteer::Issue.new(event['issue']))
95
+ end
92
96
  end
93
97
 
94
98
  attr_reader :client, :timeout_settings
@@ -109,7 +113,9 @@ class Puppeteer::FrameManager
109
113
  Puppeteer::AsyncUtils.await_promise_all(
110
114
  client.async_send_message('Page.setLifecycleEventsEnabled', enabled: true),
111
115
  client.async_send_message('Runtime.enable'),
116
+ @page.browser.issues_enabled? ? client.async_send_message('Audits.enable') : nil,
112
117
  )
118
+ maybe_setup_block_list(client)
113
119
  ensure_isolated_world(client, UTILITY_WORLD_NAME)
114
120
  @network_manager.init unless cdp_session
115
121
  rescue => err
@@ -224,7 +230,11 @@ class Puppeteer::FrameManager
224
230
  session = target.session
225
231
  frame&.send(:update_client, session)
226
232
  setup_listeners(session)
227
- async_init(target.target_info.target_id, session)
233
+ Async do
234
+ async_init(target.target_info.target_id, session).wait
235
+ rescue => err
236
+ debug_puts(err)
237
+ end
228
238
  end
229
239
 
230
240
  # @param event [Hash]
@@ -493,6 +503,7 @@ class Puppeteer::FrameManager
493
503
  # @pram session [Puppeteer::CDPSession]
494
504
  def handle_execution_context_created(context_payload, session)
495
505
  frame = if_present(context_payload.dig('auxData', 'frameId')) { |frame_id| @frames[frame_id] }
506
+ origin = context_payload['origin']
496
507
 
497
508
  world = nil
498
509
  if frame
@@ -508,6 +519,17 @@ class Puppeteer::FrameManager
508
519
  # connections so we might end up creating multiple isolated worlds.
509
520
  # We can use either.
510
521
  world = frame.puppeteer_world
522
+ elsif extension_origin?(origin)
523
+ extension_id = extract_extension_id(origin)
524
+ if extension_id
525
+ world = frame.extension_worlds[extension_id]
526
+ unless world
527
+ world = Puppeteer::IsolaatedWorld.new(frame._client || @client, self, frame, @timeout_settings)
528
+ frame.extension_worlds[extension_id] = world
529
+ end
530
+ world.origin = origin
531
+ world.world_id = extension_id
532
+ end
511
533
  end
512
534
  end
513
535
 
@@ -523,6 +545,19 @@ class Puppeteer::FrameManager
523
545
  @context_id_to_context[key] = context
524
546
  end
525
547
 
548
+ private def extension_origin?(origin)
549
+ origin.is_a?(String) && origin.start_with?(CHROME_EXTENSION_PREFIX)
550
+ end
551
+
552
+ private def extract_extension_id(origin)
553
+ return nil unless extension_origin?(origin)
554
+
555
+ path_part = origin[CHROME_EXTENSION_PREFIX.length..]
556
+ return nil unless path_part
557
+ slash_index = path_part.index('/')
558
+ slash_index ? path_part[0...slash_index] : path_part
559
+ end
560
+
526
561
  # @param execution_context_id [Integer]
527
562
  # @param session [Puppeteer::CDPSEssion]
528
563
  def handle_execution_context_destroyed(execution_context_id, session)
@@ -537,7 +572,7 @@ class Puppeteer::FrameManager
537
572
  def handle_execution_contexts_cleared(session)
538
573
  session_id = session.id
539
574
  @context_id_to_context.select! do |execution_context_id, context|
540
- key_session_id, context_id = execution_context_id.split(':', 2)
575
+ key_session_id, _context_id = execution_context_id.split(':', 2)
541
576
  # Make sure to only clear execution contexts that belong
542
577
  # to the current session.
543
578
  if key_session_id != session_id
@@ -554,6 +589,31 @@ class Puppeteer::FrameManager
554
589
  @context_id_to_context[key] or raise "INTERNAL ERROR: missing context with id = #{context_id}"
555
590
  end
556
591
 
592
+ private def maybe_setup_block_list(client)
593
+ block_list = @page.browser.block_list
594
+ return if block_list.nil? || block_list.empty?
595
+
596
+ client.send_message('Network.enable')
597
+ matched_network_conditions = block_list.map do |pattern|
598
+ {
599
+ urlPattern: pattern,
600
+ latency: 0,
601
+ downloadThroughput: -1,
602
+ uploadThroughput: -1,
603
+ }
604
+ end
605
+ client.send_message('Network.emulateNetworkConditionsByRule', {
606
+ matchedNetworkConditions: matched_network_conditions,
607
+ offline: true,
608
+ })
609
+ rescue Puppeteer::Connection::ProtocolError => err
610
+ if err.message.include?('Method not available') || err.message.include?("wasn't found")
611
+ client.send_message('Network.setBlockedURLs', urls: block_list)
612
+ else
613
+ raise
614
+ end
615
+ end
616
+
557
617
  # @param {!Frame} frame
558
618
  private def remove_frame_recursively(frame)
559
619
  frame.child_frames.each do |child|
@@ -1,5 +1,7 @@
1
1
  # rbs_inline: enabled
2
2
 
3
+ require 'base64'
4
+
3
5
  class Puppeteer::HTTPRequest
4
6
  include Puppeteer::DebugPrint
5
7
  include Puppeteer::IfPresent
@@ -101,7 +103,7 @@ class Puppeteer::HTTPRequest
101
103
  resource_type = event['type'] || event['resourceType'] || 'other'
102
104
  @resource_type = resource_type.downcase
103
105
  @method = event['request']['method']
104
- @post_data = event['request']['postData']
106
+ @post_data = parse_post_data(event)
105
107
  has_post_data = event.dig('request', 'hasPostData')
106
108
  @has_post_data = has_post_data.nil? ? !@post_data.nil? : has_post_data
107
109
  @frame = frame
@@ -160,6 +162,18 @@ class Puppeteer::HTTPRequest
160
162
  end
161
163
  end
162
164
 
165
+ private def parse_post_data(event)
166
+ post_data_entries = event.dig('request', 'postDataEntries')
167
+ if post_data_entries && !post_data_entries.empty?
168
+ post_data_entries
169
+ .filter_map { |entry| entry['bytes'] ? Base64.decode64(entry['bytes']) : nil }
170
+ .join
171
+ .force_encoding('UTF-8')
172
+ else
173
+ event.dig('request', 'postData')
174
+ end
175
+ end
176
+
163
177
  private def assert_interception_not_handled
164
178
  if @interception_handled
165
179
  raise AlreadyHandledError.new
@@ -113,7 +113,12 @@ class Puppeteer::HTTPResponse
113
113
  # @param text [String]
114
114
  # @rbs return: String -- Response body as text
115
115
  def text
116
- buffer
116
+ content = buffer
117
+ content = content.dup.force_encoding('UTF-8')
118
+ unless content.valid_encoding?
119
+ raise Puppeteer::Error.new('Could not decode response body as UTF-8')
120
+ end
121
+ content
117
122
  end
118
123
 
119
124
  # @param json [Hash]
@@ -62,11 +62,13 @@ class Puppeteer::IsolaatedWorld
62
62
  @ctx_bindings = Set.new
63
63
  @detached = false
64
64
  @context = nil
65
+ @origin = nil
66
+ @world_id = nil
65
67
 
66
68
  @client.on_event('Runtime.bindingCalled', &method(:handle_binding_called))
67
69
  end
68
70
 
69
- attr_reader :frame, :task_manager
71
+ attr_reader :frame, :task_manager, :origin, :world_id
70
72
 
71
73
  # only used in Puppeteer::WaitTask#initialize
72
74
  private def _bound_functions
@@ -109,6 +111,25 @@ class Puppeteer::IsolaatedWorld
109
111
  @task_manager.terminate_all(Puppeteer::WaitTask::TerminatedError.new('waitForFunction failed: frame got detached.'))
110
112
  end
111
113
 
114
+ # @rbs origin: String -- Origin for this realm
115
+ # @rbs return: String -- Origin
116
+ def origin=(origin)
117
+ @origin = origin
118
+ end
119
+
120
+ # @rbs world_id: String -- World id for this realm
121
+ # @rbs return: String -- World id
122
+ def world_id=(world_id)
123
+ @world_id = world_id
124
+ end
125
+
126
+ # @rbs return: Puppeteer::Extension? -- Owning extension for this realm
127
+ def extension
128
+ return nil unless @world_id.is_a?(String)
129
+
130
+ frame.page.browser.extensions[@world_id]
131
+ end
132
+
112
133
  def detached?
113
134
  @detached
114
135
  end
@@ -0,0 +1,16 @@
1
+ # rbs_inline: enabled
2
+
3
+ class Puppeteer::Issue
4
+ # @rbs issue: Hash[String, untyped] -- CDP issue payload
5
+ # @rbs return: void -- No return value
6
+ def initialize(issue)
7
+ @code = issue['code']
8
+ @details = issue['details']
9
+ end
10
+
11
+ # @rbs return: String -- Issue code
12
+ attr_reader :code
13
+
14
+ # @rbs return: Hash[String, untyped] -- Issue details payload
15
+ attr_reader :details
16
+ end
@@ -26,6 +26,7 @@ class Puppeteer::JSCoverage
26
26
  @enabled = false
27
27
  @script_urls = {}
28
28
  @script_sources = {}
29
+ @script_parsed_tasks = []
29
30
  end
30
31
 
31
32
  def start(
@@ -52,9 +53,10 @@ class Puppeteer::JSCoverage
52
53
  @enabled = true
53
54
  @script_urls.clear
54
55
  @script_sources.clear
56
+ @script_parsed_tasks.clear
55
57
  @event_listeners = []
56
58
  @event_listeners << @client.add_event_listener('Debugger.scriptParsed') do |event|
57
- Async do
59
+ @script_parsed_tasks << Async do
58
60
  Puppeteer::AsyncUtils.future_with_logging { on_script_parsed(event) }.call
59
61
  end
60
62
  end
@@ -95,11 +97,22 @@ class Puppeteer::JSCoverage
95
97
  response = @client.send_message('Debugger.getScriptSource', scriptId: event['scriptId'])
96
98
  @script_urls[event['scriptId']] = url
97
99
  @script_sources[event['scriptId']] = response['scriptSource']
100
+ rescue Puppeteer::Connection::ProtocolError
101
+ # The page can navigate while we are fetching sources for coverage.
102
+ # This matches upstream behavior that ignores these transient failures.
103
+ nil
104
+ end
105
+
106
+ private def drain_script_parsed_tasks
107
+ pending_tasks = @script_parsed_tasks
108
+ @script_parsed_tasks = []
109
+ pending_tasks.each(&:wait)
98
110
  end
99
111
 
100
112
  def stop
101
113
  raise 'JSCoverage is not enabled' unless @enabled
102
114
  @enabled = false
115
+ drain_script_parsed_tasks
103
116
 
104
117
  results = Puppeteer::AsyncUtils.await_promise_all(
105
118
  @client.async_send_message('Profiler.takePreciseCoverage'),
@@ -32,7 +32,12 @@ module Puppeteer::Launcher
32
32
  @default_viewport = options.key?(:default_viewport) ? options[:default_viewport] : Puppeteer::Viewport.new(width: 800, height: 600)
33
33
  @slow_mo = options[:slow_mo] || 0
34
34
  @network_enabled = options.fetch(:network_enabled, true)
35
+ @issues_enabled = options.fetch(:issues_enabled, true)
35
36
  @protocol_timeout = options[:protocol_timeout]
37
+ @block_list = options[:block_list]
38
+ if @block_list && !@block_list.is_a?(Array)
39
+ raise ArgumentError.new('block_list must be an Array of URL patterns')
40
+ end
36
41
 
37
42
  # only for Puppeteer.connect
38
43
  @target_filter = options[:target_filter]
@@ -46,7 +51,7 @@ module Puppeteer::Launcher
46
51
  end
47
52
  end
48
53
 
49
- attr_reader :default_viewport, :slow_mo, :target_filter, :is_page_target, :network_enabled, :protocol_timeout
54
+ attr_reader :default_viewport, :slow_mo, :target_filter, :is_page_target, :network_enabled, :issues_enabled, :protocol_timeout, :block_list
50
55
 
51
56
  def ignore_https_errors?
52
57
  @ignore_https_errors
@@ -87,6 +87,8 @@ module Puppeteer::Launcher
87
87
  ignore_https_errors: @browser_options.ignore_https_errors?,
88
88
  default_viewport: @browser_options.default_viewport,
89
89
  network_enabled: @browser_options.network_enabled,
90
+ issues_enabled: @browser_options.issues_enabled,
91
+ block_list: @browser_options.block_list,
90
92
  process: runner.proc,
91
93
  close_callback: -> { runner.close },
92
94
  target_filter_callback: nil,
@@ -129,9 +131,8 @@ module Puppeteer::Launcher
129
131
  '--disable-component-update',
130
132
  '--disable-default-apps',
131
133
  '--disable-dev-shm-usage',
132
- '--disable-extensions',
133
134
  # AcceptCHFrame disabled because of crbug.com/1348106.
134
- '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints',
135
+ '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,IPH_ReadingModePageActionLabel,ReadAnythingOmniboxChip',
135
136
  '--disable-hang-monitor',
136
137
  '--disable-ipc-flooding-protection',
137
138
  '--disable-popup-blocking',
@@ -177,6 +178,20 @@ module Puppeteer::Launcher
177
178
  end
178
179
  end
179
180
 
181
+ if chrome_arg_options.enable_extensions
182
+ chrome_arguments << '--enable-unsafe-extension-debugging'
183
+ if chrome_arg_options.enable_extensions.is_a?(Array) && !chrome_arg_options.enable_extensions.empty?
184
+ extension_paths = chrome_arg_options.enable_extensions.map do |path|
185
+ File.expand_path(path)
186
+ end
187
+ joined_paths = extension_paths.join(',')
188
+ chrome_arguments << "--disable-extensions-except=#{joined_paths}"
189
+ chrome_arguments << "--load-extension=#{joined_paths}"
190
+ end
191
+ else
192
+ chrome_arguments << '--disable-extensions'
193
+ end
194
+
180
195
  if chrome_arg_options.args.all? { |arg| arg.start_with?('-') }
181
196
  chrome_arguments << 'about:blank'
182
197
  end
@@ -31,13 +31,14 @@ module Puppeteer::Launcher
31
31
  @user_data_dir = options[:user_data_dir]
32
32
  @devtools = options[:devtools] || false
33
33
  @headless = options[:headless]
34
+ @enable_extensions = options[:enable_extensions] || false
34
35
  if @headless.nil?
35
36
  @headless = !@devtools
36
37
  end
37
38
  @debugging_port = options[:debugging_port] || 0
38
39
  end
39
40
 
40
- attr_reader :args, :user_data_dir, :debugging_port
41
+ attr_reader :args, :user_data_dir, :debugging_port, :enable_extensions
41
42
 
42
43
  def headless?
43
44
  @headless
@@ -213,15 +213,16 @@ class Puppeteer::Locator
213
213
  end
214
214
 
215
215
  # @rbs value: String -- Value to fill
216
+ # @rbs typing_threshold: Integer -- Minimum length to switch to direct assignment
216
217
  # @rbs return: void -- No return value
217
- def fill(value)
218
+ def fill(value, typing_threshold: 100)
218
219
  perform_action('Locator.fill',
219
220
  conditions: [
220
221
  method(:ensure_element_is_in_viewport_if_needed),
221
222
  method(:wait_for_stable_bounding_box_if_needed),
222
223
  method(:wait_for_enabled_if_needed),
223
224
  ]) do |handle, _options|
224
- fill_element(handle, value)
225
+ fill_element(handle, value, typing_threshold: typing_threshold)
225
226
  end
226
227
  end
227
228
 
@@ -422,7 +423,7 @@ class Puppeteer::Locator
422
423
  end
423
424
  end
424
425
 
425
- private def fill_element(handle, value)
426
+ private def fill_element(handle, value, typing_threshold: 100)
426
427
  input_type = handle.evaluate(<<~JAVASCRIPT)
427
428
  el => {
428
429
  if (el instanceof HTMLSelectElement) {
@@ -461,52 +462,70 @@ class Puppeteer::Locator
461
462
  when 'select'
462
463
  handle.select(value)
463
464
  when 'contenteditable', 'typeable-input'
464
- text_to_type = handle.evaluate(<<~JAVASCRIPT, value)
465
- (input, newValue) => {
466
- const currentValue = input.isContentEditable
467
- ? input.innerText
468
- : input.value;
465
+ if value.length < typing_threshold
466
+ text_to_type = handle.evaluate(<<~JAVASCRIPT, value)
467
+ (input, newValue) => {
468
+ const currentValue = input.isContentEditable
469
+ ? input.innerText
470
+ : input.value;
471
+
472
+ if (
473
+ newValue.length <= currentValue.length ||
474
+ !newValue.startsWith(input.value)
475
+ ) {
476
+ if (input.isContentEditable) {
477
+ input.innerText = '';
478
+ } else {
479
+ input.value = '';
480
+ }
481
+ return newValue;
482
+ }
483
+ const originalValue = input.isContentEditable
484
+ ? input.innerText
485
+ : input.value;
469
486
 
470
- if (
471
- newValue.length <= currentValue.length ||
472
- !newValue.startsWith(input.value)
473
- ) {
474
487
  if (input.isContentEditable) {
475
488
  input.innerText = '';
489
+ input.innerText = originalValue;
476
490
  } else {
477
491
  input.value = '';
492
+ input.value = originalValue;
478
493
  }
479
- return newValue;
494
+ return newValue.substring(originalValue.length);
480
495
  }
481
- const originalValue = input.isContentEditable
482
- ? input.innerText
483
- : input.value;
484
-
485
- if (input.isContentEditable) {
486
- input.innerText = '';
487
- input.innerText = originalValue;
488
- } else {
489
- input.value = '';
490
- input.value = originalValue;
491
- }
492
- return newValue.substring(originalValue.length);
493
- }
494
- JAVASCRIPT
495
- text_to_type = text_to_type.to_s
496
- handle.type_text(text_to_type)
496
+ JAVASCRIPT
497
+ text_to_type = text_to_type.to_s
498
+ handle.type_text(text_to_type) unless text_to_type.empty?
499
+ else
500
+ fill_directly(handle, value)
501
+ end
497
502
  when 'other-input'
498
- handle.focus
499
- handle.evaluate(<<~JAVASCRIPT, value)
500
- (input, newValue) => {
501
- input.value = newValue;
502
- input.dispatchEvent(new Event('input', { bubbles: true }));
503
- input.dispatchEvent(new Event('change', { bubbles: true }));
504
- }
505
- JAVASCRIPT
503
+ fill_directly(handle, value)
506
504
  else
507
505
  raise Puppeteer::Error.new('Element cannot be filled out.')
508
506
  end
509
507
  end
508
+
509
+ private def fill_directly(handle, value)
510
+ handle.focus
511
+ handle.evaluate(<<~JAVASCRIPT, value)
512
+ (input, newValue) => {
513
+ const currentValue = input.isContentEditable
514
+ ? input.innerText
515
+ : input.value;
516
+ if (currentValue === newValue) {
517
+ return;
518
+ }
519
+ if (input.isContentEditable) {
520
+ input.innerText = newValue;
521
+ } else {
522
+ input.value = newValue;
523
+ }
524
+ input.dispatchEvent(new Event('input', { bubbles: true }));
525
+ input.dispatchEvent(new Event('change', { bubbles: true }));
526
+ }
527
+ JAVASCRIPT
528
+ end
510
529
  end
511
530
 
512
531
  class Puppeteer::FunctionLocator < Puppeteer::Locator
@@ -54,6 +54,10 @@ class Puppeteer::NetworkManager
54
54
  }
55
55
  end
56
56
 
57
+ def active?
58
+ @offline || @latency != 0 || @download != -1 || @upload != -1
59
+ end
60
+
57
61
  def refresh
58
62
  update_network_conditions
59
63
  end
@@ -102,7 +106,6 @@ class Puppeteer::NetworkManager
102
106
  @user_cache_disabled = nil
103
107
  @internal_network_condition = InternalNetworkCondition.new(method(:send_to_clients))
104
108
  @interception_semaphore = Async::Semaphore.new(1)
105
-
106
109
  add_client(@client)
107
110
  end
108
111
 
@@ -138,10 +141,8 @@ class Puppeteer::NetworkManager
138
141
  "#<Puppeteer::HTTPRequest #{values.join(' ')}>"
139
142
  end
140
143
 
141
- private def apply_to_clients
142
- @clients.each do |client|
143
- yield client
144
- end
144
+ private def apply_to_clients(&block)
145
+ @clients.each(&block)
145
146
  end
146
147
 
147
148
  private def ignore_client_error?(error)
@@ -192,7 +193,7 @@ class Puppeteer::NetworkManager
192
193
  if @protocol_request_interception_enabled
193
194
  safe_send_message(client, 'Fetch.enable',
194
195
  handleAuthRequests: true,
195
- patterns: [{ urlPattern: '*' }],
196
+ patterns: [{ urlPattern: '*' }]
196
197
  )
197
198
  else
198
199
  safe_send_message(client, 'Fetch.disable')
@@ -208,7 +209,9 @@ class Puppeteer::NetworkManager
208
209
  apply_user_agent(client)
209
210
  apply_protocol_cache_disabled(client)
210
211
  apply_protocol_request_interception(client)
211
- safe_send_message(client, 'Network.emulateNetworkConditions', @internal_network_condition.params)
212
+ if @internal_network_condition.active?
213
+ safe_send_message(client, 'Network.emulateNetworkConditions', @internal_network_condition.params)
214
+ end
212
215
  end
213
216
 
214
217
  # @param username [String|NilClass]
@@ -343,11 +346,9 @@ class Puppeteer::NetworkManager
343
346
  private def dispatch_intercepted_request(event, fetch_request_id, client:)
344
347
  if Async::Task.current?
345
348
  Async do
346
- begin
347
- handle_request(event, fetch_request_id, client: client)
348
- rescue => err
349
- debug_puts(err)
350
- end
349
+ handle_request(event, fetch_request_id, client: client)
350
+ rescue => err
351
+ debug_puts(err)
351
352
  end
352
353
  else
353
354
  handle_request(event, fetch_request_id, client: client)
@@ -365,6 +366,7 @@ class Puppeteer::NetworkManager
365
366
  if existing_request &&
366
367
  existing_request.url == event_url &&
367
368
  existing_request.method == event.dig('request', 'method')
369
+
368
370
  if_present(@network_event_manager.request_extra_info(network_request_id).shift) do |extra_info|
369
371
  existing_request.update_headers(extra_info['headers'])
370
372
  end
@@ -474,12 +476,10 @@ class Puppeteer::NetworkManager
474
476
  end
475
477
  end
476
478
 
477
- private def with_interception_lock
479
+ private def with_interception_lock(&block)
478
480
  return yield unless Async::Task.current?
479
481
 
480
- @interception_semaphore.acquire do
481
- yield
482
- end
482
+ @interception_semaphore.acquire(&block)
483
483
  end
484
484
 
485
485
  private def handle_request_without_network_instrumentation(event, client)