puppeteer-bidi 0.0.1.beta1

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +13 -0
  4. data/CLAUDE/README.md +158 -0
  5. data/CLAUDE/async_programming.md +158 -0
  6. data/CLAUDE/click_implementation.md +340 -0
  7. data/CLAUDE/core_layer_gotchas.md +136 -0
  8. data/CLAUDE/error_handling.md +232 -0
  9. data/CLAUDE/file_chooser.md +95 -0
  10. data/CLAUDE/frame_architecture.md +346 -0
  11. data/CLAUDE/javascript_evaluation.md +341 -0
  12. data/CLAUDE/jshandle_implementation.md +505 -0
  13. data/CLAUDE/keyboard_implementation.md +250 -0
  14. data/CLAUDE/mouse_implementation.md +140 -0
  15. data/CLAUDE/navigation_waiting.md +234 -0
  16. data/CLAUDE/porting_puppeteer.md +214 -0
  17. data/CLAUDE/query_handler.md +194 -0
  18. data/CLAUDE/rspec_pending_vs_skip.md +262 -0
  19. data/CLAUDE/selector_evaluation.md +198 -0
  20. data/CLAUDE/test_server_routes.md +263 -0
  21. data/CLAUDE/testing_strategy.md +236 -0
  22. data/CLAUDE/two_layer_architecture.md +180 -0
  23. data/CLAUDE/wrapped_element_click.md +247 -0
  24. data/CLAUDE.md +185 -0
  25. data/LICENSE.txt +21 -0
  26. data/README.md +488 -0
  27. data/Rakefile +21 -0
  28. data/lib/puppeteer/bidi/async_utils.rb +151 -0
  29. data/lib/puppeteer/bidi/browser.rb +285 -0
  30. data/lib/puppeteer/bidi/browser_context.rb +53 -0
  31. data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
  32. data/lib/puppeteer/bidi/connection.rb +182 -0
  33. data/lib/puppeteer/bidi/core/README.md +169 -0
  34. data/lib/puppeteer/bidi/core/browser.rb +230 -0
  35. data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
  36. data/lib/puppeteer/bidi/core/disposable.rb +69 -0
  37. data/lib/puppeteer/bidi/core/errors.rb +64 -0
  38. data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
  39. data/lib/puppeteer/bidi/core/navigation.rb +128 -0
  40. data/lib/puppeteer/bidi/core/realm.rb +315 -0
  41. data/lib/puppeteer/bidi/core/request.rb +300 -0
  42. data/lib/puppeteer/bidi/core/session.rb +153 -0
  43. data/lib/puppeteer/bidi/core/user_context.rb +208 -0
  44. data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
  45. data/lib/puppeteer/bidi/core.rb +45 -0
  46. data/lib/puppeteer/bidi/deserializer.rb +132 -0
  47. data/lib/puppeteer/bidi/element_handle.rb +602 -0
  48. data/lib/puppeteer/bidi/errors.rb +42 -0
  49. data/lib/puppeteer/bidi/file_chooser.rb +52 -0
  50. data/lib/puppeteer/bidi/frame.rb +597 -0
  51. data/lib/puppeteer/bidi/http_response.rb +23 -0
  52. data/lib/puppeteer/bidi/injected.js +1 -0
  53. data/lib/puppeteer/bidi/injected_source.rb +21 -0
  54. data/lib/puppeteer/bidi/js_handle.rb +302 -0
  55. data/lib/puppeteer/bidi/keyboard.rb +265 -0
  56. data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
  57. data/lib/puppeteer/bidi/mouse.rb +170 -0
  58. data/lib/puppeteer/bidi/page.rb +613 -0
  59. data/lib/puppeteer/bidi/query_handler.rb +397 -0
  60. data/lib/puppeteer/bidi/realm.rb +242 -0
  61. data/lib/puppeteer/bidi/serializer.rb +139 -0
  62. data/lib/puppeteer/bidi/target.rb +81 -0
  63. data/lib/puppeteer/bidi/task_manager.rb +44 -0
  64. data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
  65. data/lib/puppeteer/bidi/transport.rb +129 -0
  66. data/lib/puppeteer/bidi/version.rb +7 -0
  67. data/lib/puppeteer/bidi/wait_task.rb +322 -0
  68. data/lib/puppeteer/bidi.rb +49 -0
  69. data/scripts/update_injected_source.rb +57 -0
  70. data/sig/puppeteer/bidi/browser.rbs +80 -0
  71. data/sig/puppeteer/bidi/element_handle.rbs +238 -0
  72. data/sig/puppeteer/bidi/frame.rbs +205 -0
  73. data/sig/puppeteer/bidi/js_handle.rbs +90 -0
  74. data/sig/puppeteer/bidi/page.rbs +247 -0
  75. data/sig/puppeteer/bidi.rbs +15 -0
  76. metadata +176 -0
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppeteer
4
+ module Bidi
5
+ module Core
6
+ # UserPrompt represents a user prompt (alert, confirm, prompt)
7
+ class UserPrompt < EventEmitter
8
+ include Disposable::DisposableMixin
9
+
10
+ # Create a user prompt instance
11
+ # @param browsing_context [BrowsingContext] The browsing context
12
+ # @param info [Hash] The userPromptOpened event data
13
+ # @return [UserPrompt] New user prompt instance
14
+ def self.from(browsing_context, info)
15
+ prompt = new(browsing_context, info)
16
+ prompt.send(:initialize_prompt)
17
+ prompt
18
+ end
19
+
20
+ attr_reader :browsing_context, :info, :result
21
+
22
+ def initialize(browsing_context, info)
23
+ super()
24
+ @browsing_context = browsing_context
25
+ @info = info
26
+ @reason = nil
27
+ @result = nil
28
+ @disposables = Disposable::DisposableStack.new
29
+ end
30
+
31
+ # Check if the prompt is closed
32
+ def closed?
33
+ !@reason.nil?
34
+ end
35
+
36
+ alias disposed? closed?
37
+
38
+ # Check if the prompt has been handled
39
+ # Auto-handled prompts return true immediately
40
+ # @return [Boolean] Whether the prompt is handled
41
+ def handled?
42
+ handler = @info['handler']
43
+ return true if handler == 'accept' || handler == 'dismiss'
44
+ !@result.nil?
45
+ end
46
+
47
+ # Handle the user prompt
48
+ # @param accept [Boolean, nil] Whether to accept the prompt
49
+ # @param user_text [String, nil] Text to enter (for prompt dialogs)
50
+ # @return [Hash] Result of handling the prompt
51
+ def handle(accept: nil, user_text: nil)
52
+ raise UserPromptClosedError, @reason if closed?
53
+
54
+ params = { context: @info['context'] }
55
+ params[:accept] = accept unless accept.nil?
56
+ params[:userText] = user_text if user_text
57
+
58
+ session.send_command('browsingContext.handleUserPrompt', params)
59
+
60
+ # The result is set by the userPromptClosed event before this returns
61
+ @result
62
+ end
63
+
64
+ protected
65
+
66
+ def perform_dispose
67
+ @reason ||= 'User prompt closed, probably because the browsing context was destroyed'
68
+ emit(:closed, { reason: @reason })
69
+ @disposables.dispose
70
+ super
71
+ end
72
+
73
+ private
74
+
75
+ def session
76
+ @browsing_context.user_context.browser.session
77
+ end
78
+
79
+ def initialize_prompt
80
+ # Listen for browsing context closure
81
+ @browsing_context.once(:closed) do |data|
82
+ dispose_prompt("User prompt closed: #{data[:reason]}")
83
+ end
84
+
85
+ # Listen for prompt closed event
86
+ session.on('browsingContext.userPromptClosed') do |params|
87
+ next unless params['context'] == @browsing_context.id
88
+
89
+ @result = params
90
+ emit(:handled, params)
91
+ dispose_prompt('User prompt handled')
92
+ end
93
+ end
94
+
95
+ def dispose_prompt(reason)
96
+ @reason = reason
97
+ dispose
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Core module provides low-level BiDi protocol abstractions
4
+ # This layer sits above the WebSocket transport and provides
5
+ # object-oriented semantics around WebDriver BiDi's flat API.
6
+ #
7
+ # Design principles:
8
+ # - Required arguments are method parameters, optional ones are keyword arguments
9
+ # - Session is never exposed on public methods except on Browser
10
+ # - Follows WebDriver BiDi spec strictly, not Puppeteer's needs
11
+ # - Implements BiDi comprehensively but minimally
12
+
13
+ require 'puppeteer/bidi/core/errors'
14
+ require 'puppeteer/bidi/core/event_emitter'
15
+ require 'puppeteer/bidi/core/disposable'
16
+ require 'puppeteer/bidi/core/request'
17
+ require 'puppeteer/bidi/core/navigation'
18
+ require 'puppeteer/bidi/core/realm'
19
+ require 'puppeteer/bidi/core/session'
20
+ require 'puppeteer/bidi/core/browser'
21
+ require 'puppeteer/bidi/core/user_context'
22
+ require 'puppeteer/bidi/core/browsing_context'
23
+ require 'puppeteer/bidi/core/user_prompt'
24
+
25
+ module Puppeteer
26
+ module Bidi
27
+ # Core module containing low-level BiDi protocol classes
28
+ module Core
29
+ # This module provides:
30
+ # - EventEmitter: Event subscription and emission
31
+ # - Disposable: Resource management and cleanup
32
+ # - Session: BiDi session management
33
+ # - Browser: Browser instance management
34
+ # - UserContext: Isolated browsing contexts (incognito-like)
35
+ # - BrowsingContext: Individual tabs/windows/frames
36
+ # - Navigation: Navigation tracking
37
+ # - Realm: JavaScript execution contexts
38
+ # - WindowRealm: Window/iframe realms
39
+ # - DedicatedWorkerRealm: Dedicated worker realms
40
+ # - SharedWorkerRealm: Shared worker realms
41
+ # - Request: Network request management
42
+ # - UserPrompt: User prompt (alert/confirm/prompt) handling
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'set'
5
+
6
+ module Puppeteer
7
+ module Bidi
8
+ # Deserializer converts BiDi Script.RemoteValue to Ruby values
9
+ # Based on Puppeteer's Deserializer.ts
10
+ class Deserializer
11
+ class << self
12
+ # Deserialize a BiDi RemoteValue to Ruby value
13
+ # @param remote_value [Hash] BiDi RemoteValue
14
+ # @param realm [Core::Realm, nil] Realm for creating handles (optional)
15
+ # @return [Object] Ruby value or JSHandle/ElementHandle if realm provided
16
+ def deserialize(remote_value, realm = nil)
17
+ return remote_value unless remote_value.is_a?(Hash)
18
+
19
+ type = remote_value['type']
20
+
21
+ case type
22
+ when 'string'
23
+ remote_value['value']
24
+ when 'number'
25
+ deserialize_number(remote_value['value'])
26
+ when 'boolean'
27
+ remote_value['value']
28
+ when 'null'
29
+ nil
30
+ when 'undefined'
31
+ nil
32
+ when 'bigint'
33
+ # BiDi sends bigint as string, convert to Integer
34
+ remote_value['value'].to_i
35
+ when 'array'
36
+ # Deserialize array elements
37
+ return [] unless remote_value['value']
38
+ remote_value['value'].map { |item| deserialize(item, realm) }
39
+ when 'set'
40
+ # Deserialize set elements and return Ruby Set
41
+ return Set.new unless remote_value['value']
42
+ Set.new(remote_value['value'].map { |item| deserialize(item, realm) })
43
+ when 'object'
44
+ # Object is an array of [key, value] tuples
45
+ deserialize_object(remote_value['value'], realm)
46
+ when 'map'
47
+ # Map is an array of [key, value] tuples with non-string keys
48
+ deserialize_map(remote_value['value'], realm)
49
+ when 'regexp'
50
+ # Reconstruct Ruby Regexp
51
+ deserialize_regexp(remote_value['value'])
52
+ when 'date'
53
+ # Parse ISO 8601 datetime string
54
+ Time.parse(remote_value['value'])
55
+ when 'promise'
56
+ # Promise placeholder - return empty hash
57
+ {}
58
+ when 'node'
59
+ # DOM node - create ElementHandle if realm provided
60
+ if realm
61
+ ElementHandle.new(realm, remote_value)
62
+ else
63
+ # Without realm, return the remote value as-is
64
+ remote_value
65
+ end
66
+ else
67
+ # Unknown type - create JSHandle if realm provided
68
+ if realm
69
+ JSHandle.new(realm, remote_value)
70
+ else
71
+ # Without realm, return value as-is or nil
72
+ remote_value['value']
73
+ end
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # Deserialize a BiDi number value (handles special strings)
80
+ def deserialize_number(value)
81
+ case value
82
+ when 'NaN'
83
+ Float::NAN
84
+ when 'Infinity'
85
+ Float::INFINITY
86
+ when '-Infinity'
87
+ -Float::INFINITY
88
+ when '-0'
89
+ -0.0
90
+ else
91
+ value
92
+ end
93
+ end
94
+
95
+ # Deserialize BiDi object (array of [key, value] tuples) to Ruby Hash
96
+ def deserialize_object(tuples, realm)
97
+ return {} unless tuples
98
+
99
+ tuples.each_with_object({}) do |(key, value), hash|
100
+ # Keys in objects are always strings in BiDi
101
+ hash[key] = deserialize(value, realm)
102
+ end
103
+ end
104
+
105
+ # Deserialize BiDi map (array of [key, value] tuples) to Ruby Hash
106
+ # Maps can have non-string keys, so we deserialize keys too
107
+ def deserialize_map(tuples, realm)
108
+ return {} unless tuples
109
+
110
+ tuples.each_with_object({}) do |pair, hash|
111
+ key = deserialize(pair[0], realm)
112
+ value = deserialize(pair[1], realm)
113
+ hash[key] = value
114
+ end
115
+ end
116
+
117
+ # Deserialize BiDi regexp to Ruby Regexp
118
+ def deserialize_regexp(value)
119
+ pattern = value['pattern']
120
+ flags_str = value['flags'] || ''
121
+
122
+ flags = 0
123
+ flags |= Regexp::IGNORECASE if flags_str.include?('i')
124
+ flags |= Regexp::MULTILINE if flags_str.include?('m')
125
+ flags |= Regexp::EXTENDED if flags_str.include?('x')
126
+
127
+ Regexp.new(pattern, flags)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end