puppeteer-ruby 0.45.6 → 0.50.0.alpha5

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +169 -0
  4. data/CLAUDE/README.md +41 -0
  5. data/CLAUDE/architecture.md +253 -0
  6. data/CLAUDE/cdp_protocol.md +230 -0
  7. data/CLAUDE/concurrency.md +216 -0
  8. data/CLAUDE/porting_puppeteer.md +575 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1041 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +8 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +105 -56
  17. data/lib/puppeteer/aria_query_handler.rb +3 -2
  18. data/lib/puppeteer/async_utils.rb +214 -0
  19. data/lib/puppeteer/browser.rb +98 -56
  20. data/lib/puppeteer/browser_connector.rb +18 -3
  21. data/lib/puppeteer/browser_context.rb +196 -3
  22. data/lib/puppeteer/browser_runner.rb +18 -10
  23. data/lib/puppeteer/cdp_session.rb +67 -23
  24. data/lib/puppeteer/chrome_target_manager.rb +65 -40
  25. data/lib/puppeteer/connection.rb +55 -36
  26. data/lib/puppeteer/console_message.rb +9 -1
  27. data/lib/puppeteer/console_patch.rb +47 -0
  28. data/lib/puppeteer/css_coverage.rb +5 -3
  29. data/lib/puppeteer/custom_query_handler.rb +80 -33
  30. data/lib/puppeteer/define_async_method.rb +31 -37
  31. data/lib/puppeteer/dialog.rb +47 -14
  32. data/lib/puppeteer/element_handle.rb +231 -62
  33. data/lib/puppeteer/emulation_manager.rb +1 -1
  34. data/lib/puppeteer/env.rb +1 -1
  35. data/lib/puppeteer/errors.rb +25 -2
  36. data/lib/puppeteer/event_callbackable.rb +15 -0
  37. data/lib/puppeteer/events.rb +4 -0
  38. data/lib/puppeteer/execution_context.rb +148 -3
  39. data/lib/puppeteer/file_chooser.rb +6 -0
  40. data/lib/puppeteer/frame.rb +162 -91
  41. data/lib/puppeteer/frame_manager.rb +69 -48
  42. data/lib/puppeteer/http_request.rb +114 -38
  43. data/lib/puppeteer/http_response.rb +24 -7
  44. data/lib/puppeteer/isolated_world.rb +64 -41
  45. data/lib/puppeteer/js_coverage.rb +5 -3
  46. data/lib/puppeteer/js_handle.rb +58 -16
  47. data/lib/puppeteer/keyboard.rb +30 -17
  48. data/lib/puppeteer/launcher/browser_options.rb +3 -1
  49. data/lib/puppeteer/launcher/chrome.rb +8 -5
  50. data/lib/puppeteer/launcher/launch_options.rb +7 -2
  51. data/lib/puppeteer/launcher.rb +4 -8
  52. data/lib/puppeteer/lifecycle_watcher.rb +38 -22
  53. data/lib/puppeteer/mouse.rb +273 -64
  54. data/lib/puppeteer/network_event_manager.rb +7 -0
  55. data/lib/puppeteer/network_manager.rb +393 -112
  56. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  57. data/lib/puppeteer/page.rb +568 -226
  58. data/lib/puppeteer/puppeteer.rb +171 -64
  59. data/lib/puppeteer/query_handler_manager.rb +112 -16
  60. data/lib/puppeteer/reactor_runner.rb +247 -0
  61. data/lib/puppeteer/remote_object.rb +127 -47
  62. data/lib/puppeteer/target.rb +74 -27
  63. data/lib/puppeteer/task_manager.rb +3 -1
  64. data/lib/puppeteer/timeout_helper.rb +6 -10
  65. data/lib/puppeteer/touch_handle.rb +39 -0
  66. data/lib/puppeteer/touch_screen.rb +72 -22
  67. data/lib/puppeteer/tracing.rb +3 -3
  68. data/lib/puppeteer/version.rb +1 -1
  69. data/lib/puppeteer/wait_task.rb +264 -101
  70. data/lib/puppeteer/web_socket.rb +2 -2
  71. data/lib/puppeteer/web_socket_transport.rb +91 -27
  72. data/lib/puppeteer/web_worker.rb +175 -0
  73. data/lib/puppeteer.rb +20 -4
  74. data/puppeteer-ruby.gemspec +15 -11
  75. data/sig/_external.rbs +8 -0
  76. data/sig/_supplementary.rbs +314 -0
  77. data/sig/puppeteer/browser.rbs +166 -0
  78. data/sig/puppeteer/cdp_session.rbs +64 -0
  79. data/sig/puppeteer/dialog.rbs +41 -0
  80. data/sig/puppeteer/element_handle.rbs +305 -0
  81. data/sig/puppeteer/execution_context.rbs +87 -0
  82. data/sig/puppeteer/frame.rbs +226 -0
  83. data/sig/puppeteer/http_request.rbs +214 -0
  84. data/sig/puppeteer/http_response.rbs +89 -0
  85. data/sig/puppeteer/js_handle.rbs +64 -0
  86. data/sig/puppeteer/keyboard.rbs +40 -0
  87. data/sig/puppeteer/mouse.rbs +113 -0
  88. data/sig/puppeteer/page.rbs +515 -0
  89. data/sig/puppeteer/puppeteer.rbs +98 -0
  90. data/sig/puppeteer/remote_object.rbs +78 -0
  91. data/sig/puppeteer/touch_handle.rbs +21 -0
  92. data/sig/puppeteer/touch_screen.rbs +35 -0
  93. data/sig/puppeteer/web_worker.rbs +83 -0
  94. metadata +116 -45
  95. data/CHANGELOG.md +0 -397
  96. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  97. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  98. data/lib/puppeteer/launcher/firefox.rb +0 -453
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'console/event/failure'
5
+ require 'console/terminal/formatter/failure'
6
+ rescue LoadError
7
+ # Console is optional; skip patching if unavailable.
8
+ end
9
+
10
+ module Console
11
+ module Event
12
+ class Failure
13
+ unless method_defined?(:extract_without_cycle_guard)
14
+ alias extract_without_cycle_guard extract
15
+
16
+ private def extract(exception, hash)
17
+ seen = Thread.current[:console_failure_seen] ||= {}
18
+ return if seen[exception.object_id]
19
+
20
+ seen[exception.object_id] = true
21
+ begin
22
+ extract_without_cycle_guard(exception, hash)
23
+ ensure
24
+ seen.delete(exception.object_id)
25
+ Thread.current[:console_failure_seen] = nil if seen.empty?
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ module Terminal
33
+ module Formatter
34
+ class Failure
35
+ unless method_defined?(:format_without_nil_guard)
36
+ alias format_without_nil_guard format
37
+
38
+ def format(event, stream, prefix: nil, verbose: false, **options)
39
+ event = event.dup
40
+ event[:message] = event[:message].to_s
41
+ format_without_nil_guard(event, stream, prefix: prefix, verbose: verbose, **options)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -35,12 +35,14 @@ class Puppeteer::CSSCoverage
35
35
  @stylesheet_sources.clear
36
36
  @event_listeners = []
37
37
  @event_listeners << @client.add_event_listener('CSS.styleSheetAdded') do |event|
38
- future { on_stylesheet(event) }
38
+ Async do
39
+ Puppeteer::AsyncUtils.future_with_logging { on_stylesheet(event) }.call
40
+ end
39
41
  end
40
42
  @event_listeners << @client.add_event_listener('Runtime.executionContextsCleared') do
41
43
  on_execution_contexts_cleared
42
44
  end
43
- await_all(
45
+ Puppeteer::AsyncUtils.await_promise_all(
44
46
  @client.async_send_message('DOM.enable'),
45
47
  @client.async_send_message('CSS.enable'),
46
48
  @client.async_send_message('CSS.startRuleUsageTracking'),
@@ -76,7 +78,7 @@ class Puppeteer::CSSCoverage
76
78
  @enabled = false
77
79
 
78
80
  rule_tracking_response = @client.send_message('CSS.stopRuleUsageTracking')
79
- await_all(
81
+ Puppeteer::AsyncUtils.await_promise_all(
80
82
  @client.async_send_message('CSS.disable'),
81
83
  @client.async_send_message('DOM.disable'),
82
84
  )
@@ -7,21 +7,23 @@ class Puppeteer::CustomQueryHandler
7
7
  end
8
8
 
9
9
  def query_one(element, selector)
10
- unless @query_one
11
- raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
10
+ if @query_one
11
+ return query_one_with_query_one(element, selector)
12
12
  end
13
13
 
14
- handle = element.evaluate_handle(@query_one, selector)
15
- element = handle.as_element
14
+ if @query_all
15
+ elements = query_all_with_query_all(element, selector)
16
+ return nil if elements.empty?
16
17
 
17
- if element
18
- return element
18
+ first = elements.shift
19
+ elements.each(&:dispose)
20
+ return first
19
21
  end
20
- handle.dispose
21
- nil
22
+
23
+ raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
22
24
  end
23
25
 
24
- def wait_for(element_or_frame, selector, visible: nil, hidden: nil, timeout: nil)
26
+ def wait_for(element_or_frame, selector, visible: nil, hidden: nil, timeout: nil, polling: nil)
25
27
  case element_or_frame
26
28
  when Puppeteer::Frame
27
29
  frame = element_or_frame
@@ -37,42 +39,87 @@ class Puppeteer::CustomQueryHandler
37
39
  raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
38
40
  end
39
41
 
40
- result = frame.puppeteer_world.send(:wait_for_selector_in_page,
41
- @query_one,
42
- element,
43
- selector,
44
- visible: visible,
45
- hidden: hidden,
46
- timeout: timeout,
47
- )
42
+ begin
43
+ result = frame.puppeteer_world.send(:wait_for_selector_in_page,
44
+ @query_one,
45
+ element,
46
+ selector,
47
+ visible: visible,
48
+ hidden: hidden,
49
+ timeout: timeout,
50
+ polling: polling,
51
+ )
48
52
 
49
- element&.dispose
50
-
51
- if result.is_a?(Puppeteer::ElementHandle)
52
- result.frame.main_world.transfer_handle(result)
53
- else
54
- result&.dispose
55
- nil
53
+ if result.is_a?(Puppeteer::ElementHandle)
54
+ result.frame.main_world.transfer_handle(result)
55
+ else
56
+ result&.dispose
57
+ nil
58
+ end
59
+ rescue => err
60
+ wait_for_selector_error =
61
+ if err.is_a?(Puppeteer::TimeoutError)
62
+ Puppeteer::TimeoutError.new("Waiting for selector `#{selector}` failed")
63
+ else
64
+ Puppeteer::Error.new("Waiting for selector `#{selector}` failed")
65
+ end
66
+ wait_for_selector_error.cause = err
67
+ raise wait_for_selector_error
68
+ ensure
69
+ element&.dispose
56
70
  end
57
71
  end
58
72
 
59
73
  def query_all(element, selector)
60
- unless @query_all
61
- raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
74
+ if @query_all
75
+ return query_all_with_query_all(element, selector)
62
76
  end
63
77
 
64
- handles = element.evaluate_handle(@query_all, selector)
65
- properties = handles.properties
66
- handles.dispose
67
- properties.values.map(&:as_element).compact
78
+ if @query_one
79
+ element_handle = query_one_with_query_one(element, selector)
80
+ return element_handle ? [element_handle] : []
81
+ end
82
+
83
+ raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
68
84
  end
69
85
 
70
86
  def query_all_array(element, selector)
71
- unless @query_all
72
- raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
87
+ if @query_all
88
+ handles = element.evaluate_handle(@query_all, selector)
89
+ begin
90
+ return handles.evaluate_handle('(res) => Array.from(res)')
91
+ ensure
92
+ handles.dispose
93
+ end
94
+ end
95
+
96
+ if @query_one
97
+ elements = query_all(element, selector)
98
+ begin
99
+ return element.execution_context.evaluate_handle('(...elements) => elements', *elements)
100
+ ensure
101
+ elements.each(&:dispose)
102
+ end
103
+ end
104
+
105
+ raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
106
+ end
107
+
108
+ private def query_one_with_query_one(element, selector)
109
+ handle = element.evaluate_handle(@query_one, selector)
110
+ element = handle.as_element
111
+
112
+ if element
113
+ return element
73
114
  end
115
+ handle.dispose
116
+ nil
117
+ end
74
118
 
119
+ private def query_all_with_query_all(element, selector)
75
120
  handles = element.evaluate_handle(@query_all, selector)
76
- handles.evaluate_handle('(res) => Array.from(res)')
121
+ properties = handles.properties
122
+ handles.dispose
123
+ properties.values.map(&:as_element).compact
77
124
  end
78
125
  end
@@ -17,40 +17,42 @@ module Puppeteer::DefineAsyncMethod
17
17
  if method_defined?(original_method_name) && original_method_name.start_with?('wait_for_')
18
18
  # def wait_for_xxx(xx, yy, &block)
19
19
  #
20
- # -> await_all(
21
- # async_wait_for_xxx(xx, yy),
22
- # future { block.call },
23
- # ).first
20
+ # -> start wait_for_xxx in a child task
21
+ # -> run block (awaiting its result if needed)
22
+ # -> wait for wait_for_xxx task, cancel on block errors
24
23
  define_method(original_method_name) do |*args, **kwargs, &block|
25
24
  if block
26
- async_method_call =
27
- if kwargs.empty? # for Ruby 2.6
28
- Concurrent::Promises.future do
25
+ runner = lambda do
26
+ parent_task = Async::Task.current
27
+ wait_task = parent_task.async do
28
+ if kwargs.empty?
29
29
  original_method.bind(self).call(*args)
30
- rescue => err
31
- Logger.new($stderr).warn(err)
32
- raise err
33
- end
34
- else
35
- Concurrent::Promises.future do
30
+ else
36
31
  original_method.bind(self).call(*args, **kwargs)
37
- rescue => err
38
- Logger.new($stderr).warn(err)
39
- raise err
40
32
  end
41
33
  end
42
34
 
43
- async_block_call = Concurrent::Promises.delay do
44
- block.call
45
- rescue => err
46
- Logger.new($stderr).warn(err)
47
- raise err
35
+ begin
36
+ block_result = block.call
37
+ Puppeteer::AsyncUtils.await(block_result)
38
+ rescue Exception => err
39
+ begin
40
+ wait_task.stop
41
+ Puppeteer::AsyncUtils.async_timeout(1000, -> { wait_task.wait }).wait
42
+ rescue Exception
43
+ # Swallow cancellation errors/timeouts; original error takes priority.
44
+ end
45
+ raise err
46
+ end
47
+
48
+ wait_task.wait
48
49
  end
49
50
 
50
- Concurrent::Promises.zip(
51
- async_method_call,
52
- async_block_call,
53
- ).value!.first
51
+ if Async::Task.current?
52
+ runner.call
53
+ else
54
+ Sync { runner.call }
55
+ end
54
56
  else
55
57
  if kwargs.empty? # for Ruby 2.6
56
58
  original_method.bind(self).call(*args)
@@ -62,20 +64,12 @@ module Puppeteer::DefineAsyncMethod
62
64
  end
63
65
 
64
66
  define_method(async_method_name) do |*args, **kwargs|
65
- if kwargs.empty? # for Ruby 2.6
66
- Concurrent::Promises.future do
67
+ Async do
68
+ if kwargs.empty? # for Ruby 2.6
67
69
  original_method.bind(self).call(*args)
68
- rescue => err
69
- Logger.new($stderr).warn(err)
70
- raise err
71
- end.extend(Puppeteer::ConcurrentRubyUtils::ConcurrentPromisesFutureExtension)
72
- else
73
- Concurrent::Promises.future do
70
+ else
74
71
  original_method.bind(self).call(*args, **kwargs)
75
- rescue => err
76
- Logger.new($stderr).warn(err)
77
- raise err
78
- end.extend(Puppeteer::ConcurrentRubyUtils::ConcurrentPromisesFutureExtension)
72
+ end
79
73
  end
80
74
  end
81
75
  end
@@ -1,34 +1,67 @@
1
+ # rbs_inline: enabled
2
+
1
3
  class Puppeteer::Dialog
2
- def initialize(client, type:, message:, default_value:)
3
- @client = client
4
+ # @rbs type: String -- Dialog type
5
+ # @rbs message: String -- Dialog message
6
+ # @rbs default_value: String? -- Default prompt value, if any
7
+ # @rbs return: void -- No return value
8
+ def initialize(type:, message:, default_value: '')
4
9
  @type = type
5
10
  @message = message
6
11
  @default_value = default_value || ''
12
+ @handled = false
7
13
  end
8
14
 
9
- attr_reader :type, :message, :default_value
15
+ attr_reader :type #: String
16
+ attr_reader :message #: String
17
+ attr_reader :default_value #: String
10
18
 
11
- # @param prompt_text - optional text that will be entered in the dialog
12
- # prompt. Has no effect if the dialog's type is not `prompt`.
13
- #
14
- # @returns A promise that resolves when the dialog has been accepted.
19
+ # @rbs prompt_text: String? -- Text entered into the prompt
20
+ # @rbs return: void -- No return value
15
21
  def accept(prompt_text = nil)
16
22
  if @handled
17
23
  raise 'Cannot accept dialog which is already handled!'
18
24
  end
19
25
  @handled = true
20
- @client.send_message('Page.handleJavaScriptDialog', {
21
- accept: true,
22
- promptText: prompt_text,
23
- }.compact)
26
+ handle(accept: true, text: prompt_text)
24
27
  end
25
28
 
26
- # @returns A promise which will resolve once the dialog has been dismissed
29
+ # @rbs return: void -- No return value
27
30
  def dismiss
28
31
  if @handled
29
- raise 'Cannot accept dialog which is already handled!'
32
+ raise 'Cannot dismiss dialog which is already handled!'
30
33
  end
31
34
  @handled = true
32
- @client.send_message('Page.handleJavaScriptDialog', accept: false)
35
+ handle(accept: false)
36
+ end
37
+
38
+ # @rbs accept: bool -- Whether to accept the dialog
39
+ # @rbs text: String? -- Text entered into the prompt
40
+ # @rbs return: void -- No return value
41
+ protected def handle(accept:, text: nil)
42
+ raise NotImplementedError
43
+ end
44
+ end
45
+
46
+ class Puppeteer::CdpDialog < Puppeteer::Dialog
47
+ # @rbs client: Puppeteer::CDPSession -- CDP session used to handle dialog
48
+ # @rbs type: String -- Dialog type
49
+ # @rbs message: String -- Dialog message
50
+ # @rbs default_value: String? -- Default prompt value, if any
51
+ # @rbs return: void -- No return value
52
+ def initialize(client, type:, message:, default_value:)
53
+ super(type: type, message: message, default_value: default_value)
54
+ @client = client
55
+ end
56
+
57
+ # @rbs accept: bool -- Whether to accept the dialog
58
+ # @rbs text: String? -- Text entered into the prompt
59
+ # @rbs return: void -- No return value
60
+ protected def handle(accept:, text: nil)
61
+ @client.send_message('Page.handleJavaScriptDialog', {
62
+ accept: accept,
63
+ promptText: text,
64
+ }.compact)
65
+ nil
33
66
  end
34
67
  end