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
@@ -1,44 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/http/endpoint"
5
+ require "async/websocket/client"
6
+
1
7
  class Puppeteer::WebSocketTransport
8
+ class ClosedError < Puppeteer::Error; end
9
+
2
10
  # @param {string} url
3
11
  # @return [Puppeteer::WebSocketTransport]
4
12
  def self.create(url)
5
- ws = Puppeteer::WebSocket.new(
6
- url: url,
7
- max_payload_size: 256 * 1024 * 1024, # 256MB
8
- )
9
- (resolvable_future do |future|
10
- ws.on_open do
11
- future.fulfill(Puppeteer::WebSocketTransport.new(ws))
12
- end
13
- ws.on_error do |error_message|
14
- future.reject(Puppeteer::WebSocket::TransportError.new(error_message))
15
- end
16
- end).value!
13
+ transport = new(url)
14
+ transport.connect.wait
15
+ transport
17
16
  end
18
17
 
19
- # @param {!WebSocket::Driver} web_socket
20
- def initialize(web_socket)
21
- @ws = web_socket
22
- @ws.on_message do |data|
23
- @on_message&.call(data)
24
- end
25
- @ws.on_close do |reason, code|
26
- @on_close&.call(reason, code)
27
- end
28
- @ws.on_error do |error|
29
- # Silently ignore all errors - we don't know what to do with them.
18
+ def initialize(url)
19
+ @url = url
20
+ @endpoint = Async::HTTP::Endpoint.parse(url)
21
+ @connection = nil
22
+ @task = nil
23
+ @closed = false
24
+ @connected = false
25
+ @on_message = nil
26
+ @on_close = nil
27
+ @connect_promise = nil
28
+ @write_mutex = Mutex.new
29
+ end
30
+
31
+ def connect
32
+ return @connect_promise if @connect_promise
33
+
34
+ @connect_promise = Async::Promise.new
35
+ @task = Async do |task|
36
+ Async::WebSocket::Client.connect(@endpoint) do |connection|
37
+ @connection = connection
38
+ @connected = true
39
+ @connect_promise.resolve(true) unless @connect_promise.resolved?
40
+ receive_loop(connection)
41
+ end
42
+ rescue Async::Stop
43
+ # Task was stopped; ignore.
44
+ rescue => err
45
+ @connect_promise.reject(err) unless @connect_promise.resolved?
46
+ close
47
+ ensure
48
+ @connected = false
30
49
  end
50
+
51
+ @connect_promise
31
52
  end
32
53
 
33
54
  # @param message [String]
34
55
  def send_text(message)
35
- @ws.send_text(message)
56
+ raise ClosedError.new("Transport is closed") if @closed
57
+
58
+ @write_mutex.synchronize do
59
+ @connection&.write(message)
60
+ @connection&.flush
61
+ end
62
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
63
+ close
64
+ raise
36
65
  end
37
66
 
38
67
  def close
39
- @ws.close
40
- rescue EOFError
41
- # ignore EOLError. The connection is already closed.
68
+ return if @closed
69
+
70
+ @closed = true
71
+ begin
72
+ @connection&.close
73
+ rescue Async::Stop
74
+ # Connection already closing; ignore.
75
+ end
76
+ @on_close&.call(nil, nil)
77
+ begin
78
+ @task&.stop
79
+ rescue Async::Stop
80
+ # Task was already stopping; ignore.
81
+ end
82
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
83
+ @on_close&.call(nil, nil)
42
84
  end
43
85
 
44
86
  def on_close(&block)
@@ -48,4 +90,26 @@ class Puppeteer::WebSocketTransport
48
90
  def on_message(&block)
49
91
  @on_message = block
50
92
  end
93
+
94
+ def connected?
95
+ @connected && !@closed
96
+ end
97
+
98
+ def closed?
99
+ @closed
100
+ end
101
+
102
+ private def receive_loop(connection)
103
+ while (message = connection.read)
104
+ next if message.nil?
105
+
106
+ @on_message&.call(message.to_str)
107
+ end
108
+ rescue Async::Stop
109
+ # Task stopped; no-op.
110
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
111
+ # Connection closed; no-op.
112
+ ensure
113
+ close unless @closed
114
+ end
51
115
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ class Puppeteer::WorkerWorld
5
+ using Puppeteer::DefineAsyncMethod
6
+
7
+ # @rbs client: Puppeteer::CDPSession -- CDP session
8
+ def initialize(client)
9
+ @client = client
10
+ @context_promise = Async::Promise.new
11
+ end
12
+
13
+ # @rbs context: Puppeteer::ExecutionContext -- Execution context to bind
14
+ # @rbs return: void -- No return value
15
+ def set_context(context)
16
+ @context_promise.resolve(context) unless @context_promise.resolved?
17
+ end
18
+
19
+ # @rbs return: Puppeteer::ExecutionContext -- Worker execution context
20
+ def execution_context
21
+ @context_promise.wait
22
+ end
23
+
24
+ # @rbs page_function: String -- Function or expression to evaluate
25
+ # @rbs args: Array[untyped] -- Arguments for evaluation
26
+ # @rbs return: untyped -- Evaluation result
27
+ def evaluate(page_function, *args)
28
+ execution_context.evaluate(page_function, *args)
29
+ end
30
+
31
+ define_async_method :async_evaluate
32
+
33
+ # @rbs page_function: String -- Function or expression to evaluate
34
+ # @rbs args: Array[untyped] -- Arguments for evaluation
35
+ # @rbs return: Puppeteer::JSHandle -- Handle to evaluation result
36
+ def evaluate_handle(page_function, *args)
37
+ execution_context.evaluate_handle(page_function, *args)
38
+ end
39
+
40
+ define_async_method :async_evaluate_handle
41
+
42
+ # @rbs return: nil -- Workers do not have frames
43
+ def frame
44
+ nil
45
+ end
46
+
47
+ # @rbs return: void -- Dispose world resources
48
+ def dispose
49
+ @context_promise = Async::Promise.new
50
+ end
51
+ end
52
+
53
+ class Puppeteer::WebWorker
54
+ include Puppeteer::EventCallbackable
55
+ using Puppeteer::DefineAsyncMethod
56
+
57
+ # @rbs url: String -- Worker URL
58
+ def initialize(url)
59
+ @url = url
60
+ @timeout_settings = Puppeteer::TimeoutSettings.new
61
+ end
62
+
63
+ # @rbs return: Puppeteer::TimeoutSettings -- Timeout settings
64
+ attr_reader :timeout_settings
65
+
66
+ # @rbs return: String -- Worker URL
67
+ def url
68
+ @url
69
+ end
70
+
71
+ # @rbs return: Puppeteer::WorkerWorld -- Main realm
72
+ def main_realm
73
+ raise NotImplementedError
74
+ end
75
+
76
+ # @rbs return: Puppeteer::CDPSession -- CDP session
77
+ def client
78
+ raise NotImplementedError
79
+ end
80
+
81
+ # @rbs page_function: String -- Function or expression to evaluate
82
+ # @rbs args: Array[untyped] -- Arguments for evaluation
83
+ # @rbs return: untyped -- Evaluation result
84
+ def evaluate(page_function, *args)
85
+ main_realm.evaluate(page_function, *args)
86
+ end
87
+
88
+ define_async_method :async_evaluate
89
+
90
+ # @rbs page_function: String -- Function or expression to evaluate
91
+ # @rbs args: Array[untyped] -- Arguments for evaluation
92
+ # @rbs return: Puppeteer::JSHandle -- Handle to evaluation result
93
+ def evaluate_handle(page_function, *args)
94
+ main_realm.evaluate_handle(page_function, *args)
95
+ end
96
+
97
+ define_async_method :async_evaluate_handle
98
+
99
+ # @rbs return: void -- Not supported
100
+ def close
101
+ raise Puppeteer::Error.new('WebWorker.close() is not supported')
102
+ end
103
+ end
104
+
105
+ class Puppeteer::CdpWebWorker < Puppeteer::WebWorker
106
+ include Puppeteer::DebugPrint
107
+
108
+ # @rbs client: Puppeteer::CDPSession -- Worker CDP session
109
+ # @rbs url: String -- Worker URL
110
+ # @rbs target_id: String -- Target ID
111
+ # @rbs target_type: String -- Target type
112
+ # @rbs console_api_called: Proc? -- Console callback
113
+ # @rbs exception_thrown: Proc? -- Exception callback
114
+ # @rbs network_manager: untyped? -- Network manager for worker requests
115
+ def initialize(client, url, target_id, target_type, console_api_called, exception_thrown, network_manager: nil)
116
+ super(url)
117
+ @client = client
118
+ @target_id = target_id
119
+ @target_type = target_type
120
+ @world = Puppeteer::WorkerWorld.new(@client)
121
+
122
+ @client.once('Runtime.executionContextCreated') do |event|
123
+ @world.set_context(Puppeteer::ExecutionContext.new(@client, event['context'], @world))
124
+ end
125
+ if console_api_called
126
+ @client.on_event('Runtime.consoleAPICalled') do |event|
127
+ console_api_called.call(@world, event)
128
+ end
129
+ end
130
+ if exception_thrown
131
+ @client.on_event('Runtime.exceptionThrown') do |event|
132
+ exception_thrown.call(event['exceptionDetails'])
133
+ end
134
+ end
135
+ @client.once(CDPSessionEmittedEvents::Disconnected) do
136
+ @world.dispose
137
+ end
138
+
139
+ if network_manager
140
+ Async do
141
+ begin
142
+ network_manager.add_client(@client)
143
+ rescue => err
144
+ debug_puts(err)
145
+ end
146
+ end
147
+ end
148
+
149
+ @client.async_send_message('Runtime.enable')
150
+ end
151
+
152
+ # @rbs return: Puppeteer::WorkerWorld -- Main realm
153
+ def main_realm
154
+ @world
155
+ end
156
+
157
+ # @rbs return: Puppeteer::CDPSession -- Worker CDP session
158
+ def client
159
+ @client
160
+ end
161
+
162
+ # @rbs return: void -- Close the worker
163
+ def close
164
+ connection = @client.connection
165
+ case @target_type
166
+ when 'service_worker'
167
+ connection&.send_message('Target.closeTarget', targetId: @target_id)
168
+ connection&.send_message('Target.detachFromTarget', sessionId: @client.id)
169
+ when 'shared_worker'
170
+ connection&.send_message('Target.closeTarget', targetId: @target_id)
171
+ else
172
+ evaluate('() => self.close()')
173
+ end
174
+ end
175
+ end
data/lib/puppeteer.rb CHANGED
@@ -1,4 +1,19 @@
1
- require 'concurrent'
1
+ require "async"
2
+ require 'puppeteer/console_patch'
3
+
4
+ # Check for Ruby versions affected by https://bugs.ruby-lang.org/issues/20907
5
+ # which causes hangs due to "Attempt to unlock a mutex which is not locked" errors.
6
+ # Fixed in: Ruby 3.2.7+, 3.3.7+, 3.4+
7
+ ruby_version = Gem::Version.new(RUBY_VERSION)
8
+ if ruby_version >= Gem::Version.new('3.2.0') && ruby_version < Gem::Version.new('3.2.7')
9
+ raise "Ruby #{RUBY_VERSION} has a known issue that causes puppeteer-ruby to hang. " \
10
+ "Please upgrade to Ruby 3.2.7+ or 3.3.7+ or 3.4+. " \
11
+ "See: https://github.com/socketry/async/issues/424"
12
+ elsif ruby_version >= Gem::Version.new('3.3.0') && ruby_version < Gem::Version.new('3.3.7')
13
+ raise "Ruby #{RUBY_VERSION} has a known issue that causes puppeteer-ruby to hang. " \
14
+ "Please upgrade to Ruby 3.3.7+ or 3.4+. " \
15
+ "See: https://github.com/socketry/async/issues/424"
16
+ end
2
17
 
3
18
  module Puppeteer; end
4
19
 
@@ -11,11 +26,12 @@ require 'puppeteer/geolocation'
11
26
  require 'puppeteer/viewport'
12
27
 
13
28
  # Modules
14
- require 'puppeteer/concurrent_ruby_utils'
29
+ require "puppeteer/async_utils"
15
30
  require 'puppeteer/define_async_method'
16
31
  require 'puppeteer/debug_print'
17
32
  require 'puppeteer/event_callbackable'
18
33
  require 'puppeteer/if_present'
34
+ require "puppeteer/reactor_runner"
19
35
 
20
36
  # Classes & values.
21
37
  require 'puppeteer/aria_query_handler'
@@ -37,7 +53,6 @@ require 'puppeteer/exception_details'
37
53
  require 'puppeteer/executable_path_finder'
38
54
  require 'puppeteer/execution_context'
39
55
  require 'puppeteer/file_chooser'
40
- require 'puppeteer/firefox_target_manager'
41
56
  require 'puppeteer/frame'
42
57
  require 'puppeteer/frame_manager'
43
58
  require 'puppeteer/http_request'
@@ -62,10 +77,11 @@ require 'puppeteer/task_manager'
62
77
  require 'puppeteer/tracing'
63
78
  require 'puppeteer/timeout_helper'
64
79
  require 'puppeteer/timeout_settings'
80
+ require 'puppeteer/touch_handle'
65
81
  require 'puppeteer/touch_screen'
66
82
  require 'puppeteer/version'
67
83
  require 'puppeteer/wait_task'
68
- require 'puppeteer/web_socket'
84
+ require 'puppeteer/web_worker'
69
85
  require 'puppeteer/web_socket_transport'
70
86
 
71
87
  # subclasses
@@ -12,29 +12,33 @@ Gem::Specification.new do |spec|
12
12
  spec.homepage = 'https://github.com/YusukeIwaki/puppeteer-ruby'
13
13
 
14
14
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
15
- `git ls-files -z`.split("\x0").reject do |f|
15
+ git_files = `git ls-files -z`.split("\x0").reject do |f|
16
16
  f.match(%r{^(test|spec|features)/}) || f.include?(".git") || f.include?(".circleci") || f.start_with?("development/")
17
17
  end
18
+ sig_files = Dir.glob("sig/**/*.rbs")
19
+ git_files + sig_files
18
20
  end
19
21
  spec.bindir = 'exe'
20
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
23
  spec.require_paths = ['lib']
22
24
 
23
- spec.required_ruby_version = '>= 2.6'
24
- spec.add_dependency 'concurrent-ruby', '>= 1.1', '< 1.4'
25
- spec.add_dependency 'websocket-driver', '>= 0.6.0'
25
+ spec.required_ruby_version = '>= 3.2'
26
+ spec.add_dependency "async", ">= 2.35.1", "< 3.0"
27
+ spec.add_dependency "async-http", ">= 0.60", "< 1.0"
28
+ spec.add_dependency "async-websocket", ">= 0.27", "< 1.0"
29
+ spec.add_dependency 'base64'
26
30
  spec.add_dependency 'mime-types', '>= 3.0'
27
31
  spec.add_development_dependency 'bundler'
28
32
  spec.add_development_dependency 'chunky_png'
29
33
  spec.add_development_dependency 'dry-inflector'
30
34
  spec.add_development_dependency 'pry-byebug'
31
- spec.add_development_dependency 'rake', '~> 13.1.0'
32
- spec.add_development_dependency 'rollbar'
33
- spec.add_development_dependency 'rspec', '~> 3.12.0'
35
+ spec.add_development_dependency 'rake', '~> 13.3.1'
36
+ spec.add_development_dependency 'rspec', '~> 3.13.2'
34
37
  spec.add_development_dependency 'rspec_junit_formatter' # for CircleCI.
35
- spec.add_development_dependency 'rubocop', '~> 1.50.0'
36
- spec.add_development_dependency 'rubocop-rspec'
37
- spec.add_development_dependency 'sinatra', '< 4.0.0'
38
+ spec.add_development_dependency 'rbs-inline'
39
+ spec.add_development_dependency 'rubocop', '~> 1.82.1'
40
+ spec.add_development_dependency 'rubocop-rspec', '~> 3.9.0'
41
+ spec.add_development_dependency 'sinatra', '< 5.0.0'
42
+ spec.add_development_dependency 'steep'
38
43
  spec.add_development_dependency 'webrick'
39
- spec.add_development_dependency 'yard'
40
44
  end
data/sig/_external.rbs ADDED
@@ -0,0 +1,8 @@
1
+ module Async
2
+ class Promise[T]
3
+ def wait: () -> T
4
+ def resolved?: () -> bool
5
+ def resolve: (T value) -> void
6
+ def reject: (untyped error) -> void
7
+ end
8
+ end