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,39 @@
1
+ # rbs_inline: enabled
2
+
3
+ class Puppeteer::TouchHandle
4
+ # @rbs touchscreen: Puppeteer::TouchScreen -- Touchscreen instance
5
+ # @rbs touch_point: Hash[Symbol, Numeric] -- Touch point payload
6
+ # @rbs return: void -- No return value
7
+ def initialize(touchscreen, touch_point)
8
+ @touchscreen = touchscreen
9
+ @touch_point = touch_point
10
+ @started = false
11
+ end
12
+
13
+ attr_reader :touch_point
14
+
15
+ # @rbs return: void -- No return value
16
+ def start
17
+ if @started
18
+ raise Puppeteer::TouchError.new('Touch has already started')
19
+ end
20
+
21
+ @touchscreen.send(:dispatch_touch_event, 'touchStart', [@touch_point])
22
+ @started = true
23
+ end
24
+
25
+ # @rbs x: Numeric -- New X coordinate
26
+ # @rbs y: Numeric -- New Y coordinate
27
+ # @rbs return: void -- No return value
28
+ def move(x, y)
29
+ @touch_point[:x] = x.round
30
+ @touch_point[:y] = y.round
31
+ @touchscreen.send(:dispatch_touch_event, 'touchMove', [@touch_point])
32
+ end
33
+
34
+ # @rbs return: void -- No return value
35
+ def end
36
+ @touchscreen.send(:dispatch_touch_event, 'touchEnd', [@touch_point])
37
+ @touchscreen.send(:remove_handle, self)
38
+ end
39
+ end
@@ -1,38 +1,88 @@
1
+ # rbs_inline: enabled
2
+
1
3
  class Puppeteer::TouchScreen
2
4
  using Puppeteer::DefineAsyncMethod
3
5
 
4
- # @param {Puppeteer.CDPSession} client
5
- # @param keyboard [Puppeteer::Keyboard]
6
+ # @rbs client: Puppeteer::CDPSession -- CDP session
7
+ # @rbs keyboard: Puppeteer::Keyboard -- Keyboard state for modifiers
8
+ # @rbs return: void -- No return value
6
9
  def initialize(client, keyboard)
7
10
  @client = client
8
11
  @keyboard = keyboard
12
+ @touch_id_counter = 0
13
+ @touches = []
9
14
  end
10
15
 
11
- # @param x [number]
12
- # @param y [number]
16
+ # @rbs x: Numeric -- X coordinate
17
+ # @rbs y: Numeric -- Y coordinate
18
+ # @rbs return: void -- No return value
13
19
  def tap(x, y)
14
- # Touches appear to be lost during the first frame after navigation.
15
- # This waits a frame before sending the tap.
16
- # @see https://crbug.com/613219
17
- @client.send_message('Runtime.evaluate',
18
- expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
19
- awaitPromise: true,
20
- )
20
+ touch = touch_start(x, y)
21
+ touch.end
22
+ end
23
+
24
+ define_async_method :async_tap
25
+
26
+ # @rbs x: Numeric -- X coordinate
27
+ # @rbs y: Numeric -- Y coordinate
28
+ # @rbs return: Puppeteer::TouchHandle -- Touch handle
29
+ def touch_start(x, y)
30
+ @touch_id_counter += 1
31
+ touch_point = {
32
+ x: x.round,
33
+ y: y.round,
34
+ radiusX: 0.5,
35
+ radiusY: 0.5,
36
+ force: 0.5,
37
+ id: @touch_id_counter,
38
+ }
39
+ touch = Puppeteer::TouchHandle.new(self, touch_point)
40
+ touch.start
41
+ @touches << touch
42
+ touch
43
+ end
44
+
45
+ define_async_method :async_touch_start
46
+
47
+ # @rbs x: Numeric -- X coordinate
48
+ # @rbs y: Numeric -- Y coordinate
49
+ # @rbs return: void -- No return value
50
+ def touch_move(x, y)
51
+ touch = @touches.first
52
+ raise Puppeteer::TouchError.new('Must start a new Touch first') unless touch
53
+
54
+ touch.move(x, y)
55
+ end
56
+
57
+ define_async_method :async_touch_move
21
58
 
22
- touch_points = [
23
- { x: x.round, y: y.round },
24
- ]
59
+ # @rbs return: void -- No return value
60
+ def touch_end
61
+ touch = @touches.shift
62
+ raise Puppeteer::TouchError.new('Must start a new Touch first') unless touch
63
+
64
+ touch.end
65
+ end
66
+
67
+ define_async_method :async_touch_end
68
+
69
+ # @rbs touch: Puppeteer::TouchHandle -- Touch handle to remove
70
+ # @rbs return: void -- No return value
71
+ private def remove_handle(touch)
72
+ index = @touches.index(touch)
73
+ return unless index
74
+
75
+ @touches.delete_at(index)
76
+ end
77
+
78
+ # @rbs type: String -- Touch event type
79
+ # @rbs touch_points: Array[Hash[Symbol, Numeric]] -- Touch points payload
80
+ # @rbs return: void -- No return value
81
+ private def dispatch_touch_event(type, touch_points)
25
82
  @client.send_message('Input.dispatchTouchEvent',
26
- type: 'touchStart',
83
+ type: type,
27
84
  touchPoints: touch_points,
28
85
  modifiers: @keyboard.modifiers,
29
86
  )
30
- @client.send_message('Input.dispatchTouchEvent',
31
- type: 'touchEnd',
32
- touchPoints: [],
33
- modifiers: @keyboard.modifiers,
34
- )
35
87
  end
36
-
37
- define_async_method :async_tap
38
88
  end
@@ -41,15 +41,15 @@ class Puppeteer::Tracing
41
41
  end
42
42
 
43
43
  def stop
44
- stream_promise = resolvable_future do |f|
44
+ stream_promise = Async::Promise.new.tap do |future|
45
45
  @client.once('Tracing.tracingComplete') do |event|
46
- f.fulfill(event['stream'])
46
+ future.resolve(event['stream'])
47
47
  end
48
48
  end
49
49
  @client.send_message('Tracing.end')
50
50
  @recording = false
51
51
 
52
- stream = await stream_promise
52
+ stream = stream_promise.wait
53
53
  chunks = Puppeteer::ProtocolStreamReader.new(client: @client, handle: stream).read_as_chunks
54
54
 
55
55
  StringIO.open do |stringio|
@@ -1,3 +1,3 @@
1
1
  module Puppeteer
2
- VERSION = '0.45.6'
2
+ VERSION = '0.50.0.alpha5'
3
3
  end
@@ -1,11 +1,11 @@
1
1
  class Puppeteer::WaitTask
2
2
  using Puppeteer::DefineAsyncMethod
3
3
 
4
- class TerminatedError < StandardError; end
4
+ class TerminatedError < Puppeteer::Error; end
5
5
 
6
6
  class TimeoutError < ::Puppeteer::TimeoutError
7
- def initialize(title:, timeout:)
8
- super("waiting for #{title} failed: timeout #{timeout}ms exceeded")
7
+ def initialize(timeout:)
8
+ super("Waiting failed: #{timeout}ms exceeded")
9
9
  end
10
10
  end
11
11
 
@@ -15,7 +15,7 @@ class Puppeteer::WaitTask
15
15
  raise ArgumentError.new("Unknown polling option: #{polling}")
16
16
  end
17
17
  elsif polling.is_a?(Numeric)
18
- unless polling.positive?
18
+ if polling < 0
19
19
  raise ArgumentError.new("Cannot poll with non-positive interval: #{polling}")
20
20
  end
21
21
  else
@@ -26,7 +26,7 @@ class Puppeteer::WaitTask
26
26
  @polling = polling
27
27
  @timeout = timeout
28
28
  @root = root
29
- @predicate_body = "return (#{predicate_body})(...args);"
29
+ @predicate_body = build_predicate_body(predicate_body)
30
30
  @args = args
31
31
  @binding_function = binding_function
32
32
  @run_count = 0
@@ -34,163 +34,326 @@ class Puppeteer::WaitTask
34
34
  if binding_function
35
35
  @dom_world.send(:_bound_functions)[binding_function.name] = binding_function
36
36
  end
37
- @promise = resolvable_future
37
+ @promise = Async::Promise.new
38
+ @poller_handle = nil
39
+ @generic_error = Puppeteer::Error.new('Waiting failed')
38
40
 
39
41
  # Since page navigation requires us to re-install the pageScript, we should track
40
42
  # timeout on our end.
41
43
  if timeout && timeout > 0
42
- timeout_error = TimeoutError.new(title: title, timeout: timeout)
43
- Concurrent::Promises.schedule(timeout / 1000.0) { terminate(timeout_error) unless @timeout_cleared }
44
+ timeout_error = TimeoutError.new(timeout: timeout)
45
+ @timeout_task = Async do |task|
46
+ task.sleep(timeout / 1000.0)
47
+ # Avoid stopping the timeout task from inside terminate/cleanup.
48
+ @timeout_task = nil
49
+ terminate(timeout_error) unless @timeout_cleared
50
+ end
44
51
  end
52
+
45
53
  async_rerun
46
54
  end
47
55
 
48
56
  # @return [Puppeteer::JSHandle]
49
57
  def await_promise
50
- @promise.value!
58
+ @promise.wait
51
59
  end
52
60
 
53
- def terminate(error)
61
+ def terminate(error = nil)
62
+ return if @terminated
63
+
54
64
  @terminated = true
55
- @promise.reject(error)
65
+ if error && !@promise.resolved?
66
+ @promise.reject(error)
67
+ end
56
68
  cleanup
57
69
  end
58
70
 
59
71
  def rerun
60
72
  run_count = (@run_count += 1)
61
- context = @dom_world.execution_context
73
+ context = nil
74
+ success = nil
75
+ error = nil
62
76
 
63
77
  return if @terminated || run_count != @run_count
64
- if @binding_function
65
- @dom_world.add_binding_to_context(context, @binding_function)
66
- end
67
- return if @terminated || run_count != @run_count
68
-
78
+ reset_poller
69
79
  begin
70
- success = context.evaluate_handle(
80
+ context = @dom_world.execution_context
81
+ if @binding_function
82
+ @dom_world.add_binding_to_context(context, @binding_function)
83
+ end
84
+ return if @terminated || run_count != @run_count
85
+
86
+ @poller_handle = context.evaluate_handle(
71
87
  WAIT_FOR_PREDICATE_PAGE_FUNCTION,
72
88
  @root,
73
89
  @predicate_body,
74
90
  @polling,
75
- @timeout,
76
91
  *@args,
77
92
  )
93
+ success = @poller_handle.evaluate_handle('poller => poller.result()')
78
94
  rescue => err
79
95
  error = err
80
96
  end
81
97
 
82
- if @terminated || run_count != @run_count
83
- if success
84
- success.dispose
98
+ return if @terminated || run_count != @run_count
99
+
100
+ if error
101
+ bad_error = get_bad_error(error)
102
+ if bad_error
103
+ @generic_error.cause = bad_error
104
+ terminate(@generic_error)
105
+ else
106
+ reset_poller
85
107
  end
86
108
  return
87
109
  end
88
110
 
89
- # Ignore timeouts in pageScript - we track timeouts ourselves.
90
- # If the frame's execution context has already changed, `frame.evaluate` will
91
- # throw an error - ignore this predicate run altogether.
92
- if !error && (@dom_world.evaluate("s => !s", success) rescue true)
93
- success.dispose
94
- return
95
- end
111
+ @promise.resolve(success) unless @promise.resolved?
112
+ cleanup
113
+ end
96
114
 
97
- # When the page is navigated, the promise is rejected.
98
- # We will try again in the new execution context.
99
- if error && error.message.include?('Execution context was destroyed')
100
- return
115
+ private def cleanup
116
+ @timeout_cleared = true
117
+ begin
118
+ @timeout_task&.stop
119
+ rescue StandardError
120
+ # Ignore errors during timeout task cleanup.
101
121
  end
122
+ reset_poller
123
+ @dom_world.task_manager.delete(self)
124
+ end
102
125
 
103
- # We could have tried to evaluate in a context which was already
104
- # destroyed.
105
- if error && error.message.include?('Cannot find context with specified id')
106
- return
126
+ private def reset_poller
127
+ poller = @poller_handle
128
+ @poller_handle = nil
129
+ return unless poller
130
+
131
+ return if @dom_world.respond_to?(:detached?) && @dom_world.detached?
132
+
133
+ begin
134
+ poller.evaluate('poller => poller.stop()')
135
+ rescue StandardError
136
+ # Ignore errors during poller cleanup.
137
+ end
138
+ begin
139
+ poller.dispose
140
+ rescue StandardError
141
+ # Ignore errors during poller cleanup.
107
142
  end
143
+ end
108
144
 
109
- if error
110
- @promise.reject(error)
145
+ private def build_predicate_body(predicate_body)
146
+ stripped = predicate_body.to_s.strip
147
+ is_function =
148
+ stripped.start_with?('function') ||
149
+ stripped.start_with?('async function') ||
150
+ stripped.include?('=>')
151
+
152
+ if is_function
153
+ "return (#{predicate_body})(...args);"
111
154
  else
112
- @promise.fulfill(success)
155
+ "return (#{predicate_body});"
113
156
  end
114
-
115
- cleanup
116
157
  end
117
158
 
118
- private def cleanup
119
- @timeout_cleared = true
120
- @dom_world.task_manager.delete(self)
159
+ private def get_bad_error(error)
160
+ message = error.message.to_s
161
+ if message.include?('Execution context is not available in detached frame')
162
+ return Puppeteer::Error.new('Waiting failed: Frame detached')
163
+ end
164
+ return nil if message.include?('Execution context was destroyed')
165
+ return nil if message.include?('Cannot find context with specified id')
166
+ return nil if message.include?('DiscardedBrowsingContextError')
167
+ return nil if message.include?('Inspected target navigated or closed')
168
+
169
+ error
121
170
  end
122
171
 
123
172
  define_async_method :async_rerun
124
173
 
125
174
  WAIT_FOR_PREDICATE_PAGE_FUNCTION = <<~JAVASCRIPT
126
- async function _(root, predicateBody, polling, timeout, ...args) {
175
+ function _(root, predicateBody, polling, ...args) {
127
176
  const predicate = new Function('...args', predicateBody);
128
- root = root || document
129
- let timedOut = false;
130
- if (timeout)
131
- setTimeout(() => (timedOut = true), timeout);
132
- if (polling === 'raf')
133
- return await pollRaf();
134
- if (polling === 'mutation')
135
- return await pollMutation();
136
- if (typeof polling === 'number')
137
- return await pollInterval(polling);
138
- /**
139
- * @return {!Promise<*>}
140
- */
141
- async function pollMutation() {
142
- const success = await predicate(root, ...args);
143
- if (success) return Promise.resolve(success);
144
- let fulfill;
145
- const result = new Promise((x) => (fulfill = x));
146
- const observer = new MutationObserver(async () => {
147
- if (timedOut) {
148
- observer.disconnect();
149
- fulfill();
177
+ const observedRoot = root || document;
178
+ if (polling === 'mutation' && typeof MutationObserver === 'undefined') {
179
+ polling = 'raf';
180
+ }
181
+
182
+ function createDeferred() {
183
+ let resolve;
184
+ let reject;
185
+ let finished = false;
186
+ const promise = new Promise((res, rej) => {
187
+ resolve = res;
188
+ reject = rej;
189
+ });
190
+ return {
191
+ promise,
192
+ resolve: (value) => {
193
+ if (finished) return;
194
+ finished = true;
195
+ resolve(value);
196
+ },
197
+ reject: (error) => {
198
+ if (finished) return;
199
+ finished = true;
200
+ reject(error);
201
+ },
202
+ finished: () => finished,
203
+ };
204
+ }
205
+
206
+ class MutationPoller {
207
+ constructor(fn, root) {
208
+ this.fn = fn;
209
+ this.root = root;
210
+ this.observer = null;
211
+ this.deferred = null;
212
+ }
213
+ async start() {
214
+ this.deferred = createDeferred();
215
+ const result = await this.fn();
216
+ if (result) {
217
+ this.deferred.resolve(result);
218
+ return;
150
219
  }
151
- const success = await predicate(root, ...args);
152
- if (success) {
153
- observer.disconnect();
154
- fulfill(success);
220
+ this.observer = new MutationObserver(async () => {
221
+ const result = await this.fn();
222
+ if (!result) {
223
+ return;
224
+ }
225
+ this.deferred.resolve(result);
226
+ await this.stop();
227
+ });
228
+ this.observer.observe(this.root, {
229
+ childList: true,
230
+ subtree: true,
231
+ attributes: true,
232
+ });
233
+ }
234
+ async stop() {
235
+ if (!this.deferred) {
236
+ return;
155
237
  }
156
- });
157
- observer.observe(root, {
158
- childList: true,
159
- subtree: true,
160
- attributes: true,
161
- });
162
- return result;
238
+ if (!this.deferred.finished()) {
239
+ this.deferred.reject(new Error('Polling stopped'));
240
+ }
241
+ if (this.observer) {
242
+ this.observer.disconnect();
243
+ this.observer = null;
244
+ }
245
+ }
246
+ result() {
247
+ if (!this.deferred) {
248
+ return Promise.reject(new Error('Polling never started'));
249
+ }
250
+ return this.deferred.promise;
251
+ }
163
252
  }
164
- async function pollRaf() {
165
- let fulfill;
166
- const result = new Promise((x) => (fulfill = x));
167
- await onRaf();
168
- return result;
169
- async function onRaf() {
170
- if (timedOut) {
171
- fulfill();
253
+
254
+ class RAFPoller {
255
+ constructor(fn) {
256
+ this.fn = fn;
257
+ this.deferred = null;
258
+ this.rafId = null;
259
+ }
260
+ async start() {
261
+ this.deferred = createDeferred();
262
+ const result = await this.fn();
263
+ if (result) {
264
+ this.deferred.resolve(result);
172
265
  return;
173
266
  }
174
- const success = await predicate(root, ...args);
175
- if (success) fulfill(success);
176
- else requestAnimationFrame(onRaf);
267
+ const poll = async () => {
268
+ if (!this.deferred || this.deferred.finished()) {
269
+ return;
270
+ }
271
+ const result = await this.fn();
272
+ if (result) {
273
+ this.deferred.resolve(result);
274
+ await this.stop();
275
+ } else {
276
+ this.rafId = requestAnimationFrame(poll);
277
+ }
278
+ };
279
+ this.rafId = requestAnimationFrame(poll);
280
+ }
281
+ async stop() {
282
+ if (!this.deferred) {
283
+ return;
284
+ }
285
+ if (!this.deferred.finished()) {
286
+ this.deferred.reject(new Error('Polling stopped'));
287
+ }
288
+ if (this.rafId) {
289
+ cancelAnimationFrame(this.rafId);
290
+ this.rafId = null;
291
+ }
292
+ }
293
+ result() {
294
+ if (!this.deferred) {
295
+ return Promise.reject(new Error('Polling never started'));
296
+ }
297
+ return this.deferred.promise;
177
298
  }
178
299
  }
179
- async function pollInterval(pollInterval) {
180
- let fulfill;
181
- const result = new Promise((x) => (fulfill = x));
182
- await onTimeout();
183
- return result;
184
- async function onTimeout() {
185
- if (timedOut) {
186
- fulfill();
300
+
301
+ class IntervalPoller {
302
+ constructor(fn, ms) {
303
+ this.fn = fn;
304
+ this.ms = ms;
305
+ this.interval = null;
306
+ this.deferred = null;
307
+ }
308
+ async start() {
309
+ this.deferred = createDeferred();
310
+ const result = await this.fn();
311
+ if (result) {
312
+ this.deferred.resolve(result);
187
313
  return;
188
314
  }
189
- const success = await predicate(root, ...args);
190
- if (success) fulfill(success);
191
- else setTimeout(onTimeout, pollInterval);
315
+ this.interval = setInterval(async () => {
316
+ const result = await this.fn();
317
+ if (!result) {
318
+ return;
319
+ }
320
+ this.deferred.resolve(result);
321
+ await this.stop();
322
+ }, this.ms);
192
323
  }
324
+ async stop() {
325
+ if (!this.deferred) {
326
+ return;
327
+ }
328
+ if (!this.deferred.finished()) {
329
+ this.deferred.reject(new Error('Polling stopped'));
330
+ }
331
+ if (this.interval) {
332
+ clearInterval(this.interval);
333
+ this.interval = null;
334
+ }
335
+ }
336
+ result() {
337
+ if (!this.deferred) {
338
+ return Promise.reject(new Error('Polling never started'));
339
+ }
340
+ return this.deferred.promise;
341
+ }
342
+ }
343
+
344
+ const runner = () => predicate(...args);
345
+ let poller;
346
+ if (polling === 'raf') {
347
+ poller = new RAFPoller(runner);
348
+ } else if (polling === 'mutation') {
349
+ poller = new MutationPoller(runner, observedRoot);
350
+ } else if (typeof polling === 'number') {
351
+ poller = new IntervalPoller(runner, polling);
352
+ } else {
353
+ throw new Error('Unknown polling option: ' + polling);
193
354
  }
355
+ poller.start();
356
+ return poller;
194
357
  }
195
358
  JAVASCRIPT
196
359
  end
@@ -45,7 +45,7 @@ class Puppeteer::WebSocket
45
45
  rescue Errno::ECONNRESET
46
46
  raise EOFError.new('closed by remote')
47
47
  end
48
-
48
+
49
49
  def dispose
50
50
  @socket.close
51
51
  end
@@ -75,7 +75,7 @@ class Puppeteer::WebSocket
75
75
  end
76
76
  end
77
77
 
78
- class TransportError < StandardError; end
78
+ class TransportError < Puppeteer::Error; end
79
79
 
80
80
  private def setup
81
81
  @ready_state = STATE_CONNECTING