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,3 +1,5 @@
1
+ # rbs_inline: enabled
2
+
1
3
  class Puppeteer::ExecutionContext
2
4
  include Puppeteer::IfPresent
3
5
  using Puppeteer::DefineAsyncMethod
@@ -109,7 +111,7 @@ class Puppeteer::ExecutionContext
109
111
  # But we don't support the syntax here.
110
112
 
111
113
  result = client.send_message('Runtime.callFunctionOn',
112
- functionDeclaration: "#{@expression}\n#{suffix}\n",
114
+ functionDeclaration: function_declaration,
113
115
  executionContextId: context_id,
114
116
  arguments: converted_args,
115
117
  returnByValue: @return_by_value,
@@ -146,6 +148,10 @@ class Puppeteer::ExecutionContext
146
148
  # if (Object.is(arg, NaN))
147
149
  # return { unserializableValue: 'NaN' };
148
150
  @args.map do |arg|
151
+ if recursive_object?(arg)
152
+ raise EvaluationError.new('Recursive objects are not allowed.')
153
+ end
154
+
149
155
  if arg && arg.is_a?(Puppeteer::JSHandle)
150
156
  if arg.context != @execution_context
151
157
  raise EvaluationError.new('JSHandles can be evaluated only in the context they were created!')
@@ -154,12 +160,141 @@ class Puppeteer::ExecutionContext
154
160
  end
155
161
 
156
162
  arg.remote_object.converted_arg
163
+ elsif arg.is_a?(Regexp)
164
+ { value: arg.source }
157
165
  else
158
166
  { value: arg }
159
167
  end
160
168
  end
161
169
  end
162
170
 
171
+ # @rbs value: untyped -- Value to check for cycles
172
+ # @rbs stack: Hash[Integer, bool]? -- Current stack for cycle detection
173
+ # @rbs visited: Hash[Integer, bool]? -- Already visited objects
174
+ # @rbs return: bool -- Whether the value contains cycles
175
+ private def recursive_object?(value, stack = nil, visited = nil)
176
+ return false unless value.is_a?(Hash) || value.is_a?(Array)
177
+
178
+ stack ||= {}
179
+ visited ||= {}
180
+ object_id = value.object_id
181
+ return true if stack[object_id]
182
+ return false if visited[object_id]
183
+
184
+ visited[object_id] = true
185
+ stack[object_id] = true
186
+
187
+ has_cycle =
188
+ if value.is_a?(Array)
189
+ value.any? { |item| recursive_object?(item, stack, visited) }
190
+ else
191
+ value.any? do |key, item|
192
+ recursive_object?(key, stack, visited) || recursive_object?(item, stack, visited)
193
+ end
194
+ end
195
+
196
+ stack.delete(object_id)
197
+ has_cycle
198
+ end
199
+
200
+ # @rbs return: String -- JavaScript function declaration
201
+ private def function_declaration
202
+ expression = @return_by_value ? serialized_function_declaration : @expression
203
+ "#{expression}\n#{suffix}\n"
204
+ end
205
+
206
+ # @rbs return: String -- Wrapped function with serialization
207
+ private def serialized_function_declaration
208
+ <<~JAVASCRIPT
209
+ async function(...__args) {
210
+ const fn = #{@expression};
211
+ const result = await fn.apply(globalThis, __args);
212
+ #{serialized_value_source}
213
+ }
214
+ JAVASCRIPT
215
+ end
216
+
217
+ # @rbs return: String -- JavaScript serialization helper source
218
+ private def serialized_value_source
219
+ <<~JAVASCRIPT
220
+ const OMIT = Symbol('omit');
221
+ const UNDEFINED_SENTINEL = { __puppeteer_unserializable__: true };
222
+ const NUMBER_SENTINEL = (value) => ({ __puppeteer_number__: value });
223
+ const REGEXP_SENTINEL = (source, flags) => ({ __puppeteer_regexp__: { source, flags } });
224
+ const seen = new WeakSet();
225
+ const isPlainObject = (value) => {
226
+ if (!value || typeof value !== 'object') {
227
+ return false;
228
+ }
229
+ const proto = Object.getPrototypeOf(value);
230
+ return proto === Object.prototype || proto === null;
231
+ };
232
+ const serialize = (value, allowOmit) => {
233
+ if (value === undefined) {
234
+ return allowOmit ? OMIT : UNDEFINED_SENTINEL;
235
+ }
236
+ if (typeof value === 'symbol' || typeof value === 'function') {
237
+ return UNDEFINED_SENTINEL;
238
+ }
239
+ if (typeof value === 'number') {
240
+ if (Number.isNaN(value)) {
241
+ return NUMBER_SENTINEL('NaN');
242
+ }
243
+ if (Object.is(value, -0)) {
244
+ return NUMBER_SENTINEL('-0');
245
+ }
246
+ if (value === Infinity) {
247
+ return NUMBER_SENTINEL('Infinity');
248
+ }
249
+ if (value === -Infinity) {
250
+ return NUMBER_SENTINEL('-Infinity');
251
+ }
252
+ }
253
+ if (typeof value === 'bigint') {
254
+ return value;
255
+ }
256
+ if (value === null || typeof value !== 'object') {
257
+ return value;
258
+ }
259
+ if (typeof Window !== 'undefined' && value === window) {
260
+ return UNDEFINED_SENTINEL;
261
+ }
262
+ if (typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap) {
263
+ return UNDEFINED_SENTINEL;
264
+ }
265
+ if (value instanceof RegExp) {
266
+ return REGEXP_SENTINEL(value.source, value.flags || '');
267
+ }
268
+ if (value instanceof Promise) {
269
+ return {};
270
+ }
271
+ if (seen.has(value)) {
272
+ return UNDEFINED_SENTINEL;
273
+ }
274
+ seen.add(value);
275
+ if (Array.isArray(value)) {
276
+ return value.map((item) => {
277
+ const serialized = serialize(item, false);
278
+ return serialized === OMIT ? UNDEFINED_SENTINEL : serialized;
279
+ });
280
+ }
281
+ if (!isPlainObject(value)) {
282
+ return UNDEFINED_SENTINEL;
283
+ }
284
+ const result = {};
285
+ for (const [key, val] of Object.entries(value)) {
286
+ const serialized = serialize(val, true);
287
+ if (serialized === OMIT) {
288
+ continue;
289
+ }
290
+ result[key] = serialized;
291
+ }
292
+ return result;
293
+ };
294
+ return serialize(result, false);
295
+ JAVASCRIPT
296
+ end
297
+
163
298
  # /**
164
299
  # * @param {!Error} error
165
300
  # * @return {!Protocol.Runtime.evaluateReturnValue}
@@ -180,7 +315,7 @@ class Puppeteer::ExecutionContext
180
315
  end
181
316
  end
182
317
 
183
- class EvaluationError < StandardError; end
318
+ class EvaluationError < Puppeteer::Error; end
184
319
 
185
320
  # @param return_by_value [Boolean]
186
321
  # @param page_function [String]
@@ -190,7 +325,7 @@ class Puppeteer::ExecutionContext
190
325
  # https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Object_initializer
191
326
  # But we don't support the syntax here.
192
327
  js_object =
193
- if ['=>', 'async', 'function'].any? { |keyword| page_function.include?(keyword) }
328
+ if function_string?(page_function)
194
329
  JavaScriptFunction.new(self, page_function, args, return_by_value)
195
330
  else
196
331
  JavaScriptExpression.new(self, page_function, return_by_value)
@@ -201,4 +336,14 @@ class Puppeteer::ExecutionContext
201
336
  context_id: @context_id,
202
337
  )
203
338
  end
339
+
340
+ # @rbs page_function: String -- JavaScript code to check
341
+ # @rbs return: bool -- Whether the code represents a function
342
+ private def function_string?(page_function)
343
+ stripped = page_function.lstrip
344
+ return true if page_function.include?('=>')
345
+ return true if stripped.start_with?('async function')
346
+
347
+ stripped.start_with?('function')
348
+ end
204
349
  end
@@ -25,5 +25,11 @@ class Puppeteer::FileChooser
25
25
  raise 'Cannot cancel FileChooser which is already handled!'
26
26
  end
27
27
  @handled = true
28
+ js = <<~JAVASCRIPT
29
+ (element) => {
30
+ element.dispatchEvent(new Event('cancel', { bubbles: true }));
31
+ }
32
+ JAVASCRIPT
33
+ @element.evaluate(js)
28
34
  end
29
35
  end