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,7 +1,6 @@
1
1
  require_relative './launcher/browser_options'
2
2
  require_relative './launcher/chrome'
3
3
  require_relative './launcher/chrome_arg_options'
4
- require_relative './launcher/firefox'
5
4
  require_relative './launcher/launch_options'
6
5
 
7
6
  # https://github.com/puppeteer/puppeteer/blob/main/src/node/Launcher.ts
@@ -9,19 +8,16 @@ module Puppeteer::Launcher
9
8
  # @param project_root [String]
10
9
  # @param prefereed_revision [String]
11
10
  # @param is_puppeteer_core [String]
12
- # @param product [String] 'chrome' or 'firefox'
11
+ # @param product [String] 'chrome'
13
12
  # @return [Puppeteer::Launcher::Chrome]
14
13
  module_function def new(project_root:, preferred_revision:, is_puppeteer_core:, product:)
15
14
  unless is_puppeteer_core
16
15
  product ||= ENV['PUPPETEER_PRODUCT']
17
16
  end
18
17
 
19
- if product == 'firefox'
20
- return Firefox.new(
21
- project_root: project_root,
22
- preferred_revision: preferred_revision,
23
- is_puppeteer_core: is_puppeteer_core,
24
- )
18
+ product = product.to_s if product
19
+ if product && product != 'chrome'
20
+ raise ArgumentError.new("Unsupported product: #{product}. Only 'chrome' is supported.")
25
21
  end
26
22
 
27
23
  Chrome.new(
@@ -50,12 +50,12 @@ class Puppeteer::LifecycleWatcher
50
50
  end
51
51
  end
52
52
 
53
- class FrameDetachedError < StandardError
53
+ class FrameDetachedError < Puppeteer::Error
54
54
  def initialize
55
55
  super('Navigating frame was detached')
56
56
  end
57
57
  end
58
- class TerminatedError < StandardError; end
58
+ class TerminatedError < Puppeteer::Error; end
59
59
 
60
60
  # * @param {!Puppeteer.FrameManager} frameManager
61
61
  # * @param {!Puppeteer.Frame} frame
@@ -72,6 +72,12 @@ class Puppeteer::LifecycleWatcher
72
72
  @listener_ids['client'] = @frame_manager.client.add_event_listener(CDPSessionEmittedEvents::Disconnected) do
73
73
  terminate(TerminatedError.new('Navigation failed because browser has disconnected!'))
74
74
  end
75
+ connection = @frame_manager.client.respond_to?(:connection) ? @frame_manager.client.connection : nil
76
+ if connection
77
+ @listener_ids['connection'] = connection.add_event_listener(ConnectionEmittedEvents::Disconnected) do
78
+ terminate(TerminatedError.new('Navigation failed because browser has disconnected!'))
79
+ end
80
+ end
75
81
  @listener_ids['frame_manager'] = [
76
82
  @frame_manager.add_event_listener(FrameManagerEmittedEvents::LifecycleEvent) do |_|
77
83
  check_lifecycle_complete
@@ -87,24 +93,28 @@ class Puppeteer::LifecycleWatcher
87
93
  @frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::RequestFailed, &method(:handle_request_failed)),
88
94
  ]
89
95
 
90
- @same_document_navigation_promise = resolvable_future
91
- @lifecycle_promise = resolvable_future
92
- @new_document_navigation_promise = resolvable_future
93
- @termination_promise = resolvable_future
96
+ @same_document_navigation_promise = Async::Promise.new
97
+ @lifecycle_promise = Async::Promise.new
98
+ @new_document_navigation_promise = Async::Promise.new
99
+ @termination_promise = Async::Promise.new
100
+ @navigation_response_received = Async::Promise.new.tap { |promise| promise.resolve(nil) }
94
101
  check_lifecycle_complete
95
102
  end
96
103
 
97
104
  # @param [Puppeteer::HTTPRequest] request
98
105
  def handle_request(request)
99
106
  return if request.frame != @frame || !request.navigation_request?
107
+ if @navigation_request && request.redirect_chain.empty?
108
+ return
109
+ end
100
110
  @navigation_request = request
101
111
  # Resolve previous navigation response in case there are multiple
102
112
  # navigation requests reported by the backend. This generally should not
103
113
  # happen by it looks like it's possible.
104
- @navigation_response_received.fulfill(nil) if @navigation_response_received && !@navigation_response_received.resolved?
105
- @navigation_response_received = resolvable_future
114
+ @navigation_response_received.resolve(nil) if @navigation_response_received && !@navigation_response_received.resolved?
115
+ @navigation_response_received = Async::Promise.new
106
116
  if request.response && !@navigation_response_received.resolved?
107
- @navigation_response_received.fulfill(nil)
117
+ @navigation_response_received.resolve(nil)
108
118
  end
109
119
  end
110
120
 
@@ -112,14 +122,14 @@ class Puppeteer::LifecycleWatcher
112
122
  def handle_request_failed(request)
113
123
  return if @navigation_request&.internal&.request_id != request.internal.request_id
114
124
 
115
- @navigation_response_received.fulfill(nil) unless @navigation_response_received.resolved?
125
+ @navigation_response_received.resolve(nil) unless @navigation_response_received.resolved?
116
126
  end
117
127
 
118
128
  # @param [Puppeteer::HTTPResponse] response
119
129
  def handle_response(response)
120
130
  return if @navigation_request&.internal&.request_id != response.request.internal.request_id
121
131
 
122
- @navigation_response_received.fulfill(nil) unless @navigation_response_received.resolved?
132
+ @navigation_response_received.resolve(nil) unless @navigation_response_received.resolved?
123
133
  end
124
134
 
125
135
  # @param frame [Puppeteer::Frame]
@@ -134,7 +144,7 @@ class Puppeteer::LifecycleWatcher
134
144
  # @return [Puppeteer::HTTPResponse]
135
145
  def navigation_response
136
146
  # Continue with a possibly null response.
137
- @navigation_response_received.value! rescue nil
147
+ @navigation_response_received.wait rescue nil
138
148
  if_present(@navigation_request) do |request|
139
149
  request.response
140
150
  end
@@ -142,6 +152,8 @@ class Puppeteer::LifecycleWatcher
142
152
 
143
153
  # @param error [TerminatedError]
144
154
  private def terminate(error)
155
+ return if @termination_promise.resolved?
156
+
145
157
  @termination_promise.reject(error)
146
158
  end
147
159
 
@@ -153,12 +165,12 @@ class Puppeteer::LifecycleWatcher
153
165
 
154
166
  def timeout_or_termination_promise
155
167
  if @timeout > 0
156
- future do
157
- Timeout.timeout(@timeout / 1000.0) do
158
- @termination_promise.value!
168
+ -> do
169
+ begin
170
+ Puppeteer::AsyncUtils.async_timeout(@timeout, @termination_promise).wait
171
+ rescue Async::TimeoutError
172
+ raise Puppeteer::TimeoutError.new("Navigation timeout of #{@timeout} ms exceeded")
159
173
  end
160
- rescue Timeout::Error
161
- raise Puppeteer::TimeoutError.new("Navigation timeout of #{@timeout}ms exceeded")
162
174
  end
163
175
  else
164
176
  @termination_promise
@@ -187,12 +199,12 @@ class Puppeteer::LifecycleWatcher
187
199
  private def check_lifecycle_complete
188
200
  # We expect navigation to commit.
189
201
  return unless @expected_lifecycle.completed?(@frame)
190
- @lifecycle_promise.fulfill(true) if @lifecycle_promise.pending?
191
- if @has_same_document_navigation && @same_document_navigation_promise.pending?
192
- @same_document_navigation_promise.fulfill(true)
202
+ @lifecycle_promise.resolve(true) unless @lifecycle_promise.resolved?
203
+ if @has_same_document_navigation && !@same_document_navigation_promise.resolved?
204
+ @same_document_navigation_promise.resolve(true)
193
205
  end
194
- if (@swapped || @frame.loader_id != @initial_loader_id) && @new_document_navigation_promise.pending?
195
- @new_document_navigation_promise.fulfill(true)
206
+ if (@swapped || @frame.loader_id != @initial_loader_id) && !@new_document_navigation_promise.resolved?
207
+ @new_document_navigation_promise.resolve(true)
196
208
  end
197
209
  end
198
210
 
@@ -203,6 +215,10 @@ class Puppeteer::LifecycleWatcher
203
215
  if_present(@listener_ids['frame_manager']) do |ids|
204
216
  @frame_manager.remove_event_listener(*ids)
205
217
  end
218
+ if_present(@listener_ids['connection']) do |id|
219
+ connection = @frame_manager.client.respond_to?(:connection) ? @frame_manager.client.connection : nil
220
+ connection&.remove_event_listener(id)
221
+ end
206
222
  if_present(@listener_ids['network_manager']) do |ids|
207
223
  @frame_manager.network_manager.remove_event_listener(*ids)
208
224
  end
@@ -1,3 +1,5 @@
1
+ # rbs_inline: enabled
2
+
1
3
  class Puppeteer::Mouse
2
4
  using Puppeteer::DefineAsyncMethod
3
5
 
@@ -6,124 +8,232 @@ class Puppeteer::Mouse
6
8
  LEFT = 'left'
7
9
  RIGHT = 'right'
8
10
  MIDDLE = 'middle'
11
+ BACK = 'back'
12
+ FORWARD = 'forward'
13
+ end
14
+
15
+ module ButtonFlag
16
+ NONE = 0
17
+ LEFT = 1
18
+ RIGHT = 1 << 1
19
+ MIDDLE = 1 << 2
20
+ BACK = 1 << 3
21
+ FORWARD = 1 << 4
9
22
  end
10
23
 
11
- # @param {Puppeteer.CDPSession} client
12
- # @param keyboard [Puppeteer::Keyboard]
24
+ # @rbs client: Puppeteer::CDPSession -- CDP session
25
+ # @rbs keyboard: Puppeteer::Keyboard -- Keyboard instance
26
+ # @rbs return: void -- No return value
13
27
  def initialize(client, keyboard)
14
28
  @client = client
15
29
  @keyboard = keyboard
16
30
 
17
- @x = 0
18
- @y = 0
19
- @button = Button::NONE
31
+ @base_state = {
32
+ position: {
33
+ x: 0,
34
+ y: 0,
35
+ },
36
+ buttons: ButtonFlag::NONE,
37
+ }
38
+ @transactions = []
39
+ @state_mutex = Mutex.new
40
+ @dispatch_mutex = Mutex.new
41
+ end
42
+
43
+ # @rbs return: void -- No return value
44
+ def reset
45
+ [
46
+ [ButtonFlag::RIGHT, Button::RIGHT],
47
+ [ButtonFlag::MIDDLE, Button::MIDDLE],
48
+ [ButtonFlag::LEFT, Button::LEFT],
49
+ [ButtonFlag::FORWARD, Button::FORWARD],
50
+ [ButtonFlag::BACK, Button::BACK],
51
+ ].each do |flag, button|
52
+ up(button: button) if (state[:buttons] & flag) != 0
53
+ end
54
+ if state[:position][:x] != 0 || state[:position][:y] != 0
55
+ move(0, 0)
56
+ end
20
57
  end
21
58
 
22
- # @param x [number]
23
- # @param y [number]
24
- # @param steps [number]
59
+ define_async_method :async_reset
60
+
61
+ # @rbs x: Numeric -- X coordinate
62
+ # @rbs y: Numeric -- Y coordinate
63
+ # @rbs steps: Integer? -- Number of intermediate steps
64
+ # @rbs return: void -- No return value
25
65
  def move(x, y, steps: nil)
26
66
  move_steps = (steps || 1).to_i
27
67
 
28
- from_x = @x
29
- from_y = @y
30
- @x = x
31
- @y = y
68
+ from = state[:position]
69
+ to = {
70
+ x: x,
71
+ y: y,
72
+ }
32
73
 
33
74
  return if move_steps <= 0
34
75
 
35
- move_steps.times do |i|
36
- n = i + 1
37
- @client.send_message('Input.dispatchMouseEvent',
38
- type: 'mouseMoved',
39
- button: @button,
40
- x: from_x + (@x - from_x) * n / move_steps,
41
- y: from_y + (@y - from_y) * n / move_steps,
42
- modifiers: @keyboard.modifiers,
43
- )
76
+ 1.upto(move_steps) do |i|
77
+ with_transaction do |update_state|
78
+ update_state.call(
79
+ position: {
80
+ x: from[:x] + (to[:x] - from[:x]) * i / move_steps.to_f,
81
+ y: from[:y] + (to[:y] - from[:y]) * i / move_steps.to_f,
82
+ },
83
+ )
84
+ current_state = state
85
+ buttons = current_state[:buttons]
86
+ position = current_state[:position]
87
+ @client.send_message('Input.dispatchMouseEvent',
88
+ type: 'mouseMoved',
89
+ modifiers: @keyboard.modifiers,
90
+ buttons: buttons,
91
+ button: button_from_pressed_buttons(buttons),
92
+ x: position[:x],
93
+ y: position[:y],
94
+ )
95
+ end
44
96
  end
45
97
  end
46
98
 
47
99
  define_async_method :async_move
48
100
 
49
- # @param x [number]
50
- # @param y [number]
51
- # @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
52
- def click(x, y, delay: nil, button: nil, click_count: nil)
53
- # await_all(async_move, async_down, async_up) often breaks the order of CDP commands.
54
- # D, [2020-04-15T17:09:47.895895 #88683] DEBUG -- : RECV << {"id"=>23, "result"=>{"layoutViewport"=>{"pageX"=>0, "pageY"=>1, "clientWidth"=>375, "clientHeight"=>667}, "visualViewport"=>{"offsetX"=>0, "offsetY"=>0, "pageX"=>0, "pageY"=>1, "clientWidth"=>375, "clientHeight"=>667, "scale"=>1, "zoom"=>1}, "contentSize"=>{"x"=>0, "y"=>0, "width"=>375, "height"=>2007}}, "sessionId"=>"0B09EA5E18DEE403E525B3E7FCD7E225"}
55
- # D, [2020-04-15T17:09:47.898422 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mouseReleased","button":"left","x":0,"y":0,"modifiers":0,"clickCount":1},"id":24}
56
- # D, [2020-04-15T17:09:47.899711 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mousePressed","button":"left","x":0,"y":0,"modifiers":0,"clickCount":1},"id":25}
57
- # D, [2020-04-15T17:09:47.900237 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mouseMoved","button":"left","x":187,"y":283,"modifiers":0},"id":26}
58
- # So we execute them sequential
59
- move(x, y)
60
- down(button: button, click_count: click_count)
61
- if delay
62
- sleep(delay / 1000.0)
101
+ # @rbs x: Numeric -- X coordinate
102
+ # @rbs y: Numeric -- Y coordinate
103
+ # @rbs delay: Numeric? -- Delay between down and up (ms)
104
+ # @rbs button: String? -- Mouse button
105
+ # @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
106
+ # @rbs count: Integer? -- Number of click repetitions
107
+ # @rbs return: void -- No return value
108
+ def click(x, y, delay: nil, button: nil, click_count: nil, count: nil)
109
+ warn_deprecated_click_count if !click_count.nil?
110
+ count ||= 1
111
+ click_count ||= count
112
+ if count < 1
113
+ raise Puppeteer::Error.new('Click must occur a positive number of times.')
114
+ end
115
+ # Serialize click sequences to keep event ordering stable under thread-based concurrency.
116
+ @dispatch_mutex.synchronize do
117
+ move(x, y)
118
+ if click_count == count
119
+ 1.upto(count - 1) do |i|
120
+ down(button: button, click_count: i)
121
+ up(button: button, click_count: i)
122
+ end
123
+ end
124
+ down(button: button, click_count: click_count)
125
+ if !delay.nil?
126
+ Puppeteer::AsyncUtils.sleep_seconds(delay / 1000.0)
127
+ end
128
+ up(button: button, click_count: click_count)
63
129
  end
64
- up(button: button, click_count: click_count)
65
130
  end
66
131
 
67
132
  define_async_method :async_click
68
133
 
69
- # @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
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
+ # @rbs button: String? -- Mouse button
148
+ # @rbs click_count: Integer? -- Click count to report
149
+ # @rbs return: void -- No return value
70
150
  def down(button: nil, click_count: nil)
71
- @button = button || Button::LEFT
72
- @client.send_message('Input.dispatchMouseEvent',
73
- type: 'mousePressed',
74
- button: @button,
75
- x: @x,
76
- y: @y,
77
- modifiers: @keyboard.modifiers,
78
- clickCount: click_count || 1,
79
- )
151
+ button ||= Button::LEFT
152
+ flag = button_flag(button)
153
+ with_transaction do |update_state|
154
+ update_state.call(
155
+ buttons: state[:buttons] | flag,
156
+ )
157
+ current_state = state
158
+ position = current_state[:position]
159
+ @client.send_message('Input.dispatchMouseEvent',
160
+ type: 'mousePressed',
161
+ modifiers: @keyboard.modifiers,
162
+ clickCount: click_count || 1,
163
+ buttons: current_state[:buttons],
164
+ button: button,
165
+ x: position[:x],
166
+ y: position[:y],
167
+ )
168
+ end
80
169
  end
81
170
 
82
171
  define_async_method :async_down
83
172
 
84
- # @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
173
+ # @rbs button: String? -- Mouse button
174
+ # @rbs click_count: Integer? -- Click count to report
175
+ # @rbs return: void -- No return value
85
176
  def up(button: nil, click_count: nil)
86
- @button = Button::NONE
87
- @client.send_message('Input.dispatchMouseEvent',
88
- type: 'mouseReleased',
89
- button: button || Button::LEFT,
90
- x: @x,
91
- y: @y,
92
- modifiers: @keyboard.modifiers,
93
- clickCount: click_count || 1,
94
- )
177
+ button ||= Button::LEFT
178
+ flag = button_flag(button)
179
+ with_transaction do |update_state|
180
+ update_state.call(
181
+ buttons: state[:buttons] & ~flag,
182
+ )
183
+ current_state = state
184
+ position = current_state[:position]
185
+ @client.send_message('Input.dispatchMouseEvent',
186
+ type: 'mouseReleased',
187
+ modifiers: @keyboard.modifiers,
188
+ clickCount: click_count || 1,
189
+ buttons: current_state[:buttons],
190
+ button: button,
191
+ x: position[:x],
192
+ y: position[:y],
193
+ )
194
+ end
95
195
  end
96
196
 
97
197
  define_async_method :async_up
98
198
 
99
199
  # Dispatches a `mousewheel` event.
100
200
  #
101
- # @param delta_x [Integer]
102
- # @param delta_y [Integer]
201
+ # @rbs delta_x: Numeric -- Scroll delta X
202
+ # @rbs delta_y: Numeric -- Scroll delta Y
203
+ # @rbs return: void -- No return value
103
204
  def wheel(delta_x: 0, delta_y: 0)
205
+ current_state = state
206
+ position = current_state[:position]
104
207
  @client.send_message('Input.dispatchMouseEvent',
105
208
  type: 'mouseWheel',
106
- x: @x,
107
- y: @y,
209
+ x: position[:x],
210
+ y: position[:y],
108
211
  deltaX: delta_x,
109
212
  deltaY: delta_y,
110
213
  modifiers: @keyboard.modifiers,
111
214
  pointerType: 'mouse',
215
+ buttons: current_state[:buttons],
112
216
  )
113
217
  end
114
218
 
219
+ # @rbs start: Puppeteer::ElementHandle::Point -- Drag start point
220
+ # @rbs target: Puppeteer::ElementHandle::Point -- Drag end point
221
+ # @rbs return: Hash[String, untyped] -- Drag data payload
115
222
  def drag(start, target)
116
- promise = resolvable_future do |f|
223
+ promise = Async::Promise.new.tap do |future|
117
224
  @client.once('Input.dragIntercepted') do |event|
118
- f.fulfill(event['data'])
225
+ future.resolve(event['data'])
119
226
  end
120
227
  end
121
228
  move(start.x, start.y)
122
229
  down
123
230
  move(target.x, target.y)
124
- promise.value!
231
+ promise.wait
125
232
  end
126
233
 
234
+ # @rbs target: Puppeteer::ElementHandle::Point -- Drag target point
235
+ # @rbs data: Hash[String, untyped] -- Drag data payload
236
+ # @rbs return: void -- No return value
127
237
  def drag_enter(target, data)
128
238
  @client.send_message('Input.dispatchDragEvent',
129
239
  type: 'dragEnter',
@@ -134,6 +244,9 @@ class Puppeteer::Mouse
134
244
  )
135
245
  end
136
246
 
247
+ # @rbs target: Puppeteer::ElementHandle::Point -- Drag target point
248
+ # @rbs data: Hash[String, untyped] -- Drag data payload
249
+ # @rbs return: void -- No return value
137
250
  def drag_over(target, data)
138
251
  @client.send_message('Input.dispatchDragEvent',
139
252
  type: 'dragOver',
@@ -144,6 +257,9 @@ class Puppeteer::Mouse
144
257
  )
145
258
  end
146
259
 
260
+ # @rbs target: Puppeteer::ElementHandle::Point -- Drag target point
261
+ # @rbs data: Hash[String, untyped] -- Drag data payload
262
+ # @rbs return: void -- No return value
147
263
  def drop(target, data)
148
264
  @client.send_message('Input.dispatchDragEvent',
149
265
  type: 'drop',
@@ -154,14 +270,107 @@ class Puppeteer::Mouse
154
270
  )
155
271
  end
156
272
 
273
+ # @rbs start: Puppeteer::ElementHandle::Point -- Drag start point
274
+ # @rbs target: Puppeteer::ElementHandle::Point -- Drag end point
275
+ # @rbs delay: Numeric? -- Delay before drop (ms)
276
+ # @rbs return: void -- No return value
157
277
  def drag_and_drop(start, target, delay: nil)
158
278
  data = drag(start, target)
159
279
  drag_enter(target, data)
160
280
  drag_over(target, data)
161
281
  if delay
162
- sleep(delay / 1000.0)
282
+ Puppeteer::AsyncUtils.sleep_seconds(delay / 1000.0)
163
283
  end
164
284
  drop(target, data)
165
285
  up
166
286
  end
287
+
288
+ private def state
289
+ @state_mutex.synchronize do
290
+ merged = {
291
+ position: {
292
+ x: @base_state[:position][:x],
293
+ y: @base_state[:position][:y],
294
+ },
295
+ buttons: @base_state[:buttons],
296
+ }
297
+ @transactions.each do |transaction|
298
+ if transaction.key?(:position)
299
+ merged[:position] = transaction[:position]
300
+ end
301
+ if transaction.key?(:buttons)
302
+ merged[:buttons] = transaction[:buttons]
303
+ end
304
+ end
305
+ merged
306
+ end
307
+ end
308
+
309
+ # @rbs block: Proc -- Block receiving state update callback
310
+ # @rbs return: untyped -- Block result
311
+ private def with_transaction(&block)
312
+ transaction = {}
313
+ @state_mutex.synchronize do
314
+ @transactions << transaction
315
+ end
316
+
317
+ begin
318
+ update_state = lambda do |updates|
319
+ @state_mutex.synchronize do
320
+ transaction.merge!(updates)
321
+ end
322
+ end
323
+ block.call(update_state)
324
+
325
+ @state_mutex.synchronize do
326
+ @base_state = merge_state(@base_state, transaction)
327
+ @transactions.delete(transaction)
328
+ end
329
+ rescue
330
+ @state_mutex.synchronize do
331
+ @transactions.delete(transaction)
332
+ end
333
+ raise
334
+ end
335
+ end
336
+
337
+ private def merge_state(base_state, transaction)
338
+ merged = base_state.dup
339
+ merged[:position] = transaction[:position] if transaction.key?(:position)
340
+ merged[:buttons] = transaction[:buttons] if transaction.key?(:buttons)
341
+ merged
342
+ end
343
+
344
+ private def button_flag(button)
345
+ case button
346
+ when Button::LEFT
347
+ ButtonFlag::LEFT
348
+ when Button::RIGHT
349
+ ButtonFlag::RIGHT
350
+ when Button::MIDDLE
351
+ ButtonFlag::MIDDLE
352
+ when Button::BACK
353
+ ButtonFlag::BACK
354
+ when Button::FORWARD
355
+ ButtonFlag::FORWARD
356
+ else
357
+ raise Puppeteer::Error.new("Unsupported mouse button: #{button}")
358
+ end
359
+ end
360
+
361
+ private def button_from_pressed_buttons(buttons)
362
+ if (buttons & ButtonFlag::LEFT) != 0
363
+ Button::LEFT
364
+ elsif (buttons & ButtonFlag::RIGHT) != 0
365
+ Button::RIGHT
366
+ elsif (buttons & ButtonFlag::MIDDLE) != 0
367
+ Button::MIDDLE
368
+ elsif (buttons & ButtonFlag::BACK) != 0
369
+ Button::BACK
370
+ elsif (buttons & ButtonFlag::FORWARD) != 0
371
+ Button::FORWARD
372
+ else
373
+ Button::NONE
374
+ end
375
+ end
167
376
  end
@@ -44,6 +44,7 @@ class Puppeteer::NetworkEventManager
44
44
  # handle redirects, we have to make them Arrays to represent the chain of
45
45
  # events.
46
46
  @response_received_extra_info_map = {}
47
+ @request_will_be_sent_extra_info_map = {}
47
48
  @queued_redirect_info_map = {}
48
49
  @queued_event_group_map = {}
49
50
  end
@@ -51,11 +52,16 @@ class Puppeteer::NetworkEventManager
51
52
  def forget(network_request_id)
52
53
  @request_will_be_sent_map.delete(network_request_id)
53
54
  @request_paused_map.delete(network_request_id)
55
+ @request_will_be_sent_extra_info_map.delete(network_request_id)
54
56
  @queued_event_group_map.delete(network_request_id)
55
57
  @queued_redirect_info_map.delete(network_request_id)
56
58
  @response_received_extra_info_map.delete(network_request_id)
57
59
  end
58
60
 
61
+ def request_extra_info(network_request_id)
62
+ @request_will_be_sent_extra_info_map[network_request_id] ||= []
63
+ end
64
+
59
65
  def response_extra_info(network_request_id)
60
66
  @response_received_extra_info_map[network_request_id] ||= []
61
67
  end
@@ -108,6 +114,7 @@ class Puppeteer::NetworkEventManager
108
114
  @http_requests_map[network_request_id]
109
115
  end
110
116
 
117
+
111
118
  def forget_request(network_request_id)
112
119
  @http_requests_map.delete(network_request_id)
113
120
  end