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,302 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Puppeteer
5
+ module Bidi
6
+ # JSHandle represents a reference to a JavaScript object
7
+ # Based on Puppeteer's BidiJSHandle implementation
8
+ class JSHandle
9
+ attr_reader :realm #: Core::Realm
10
+
11
+ # @rbs remote_value: Hash[String, untyped]
12
+ # @rbs realm: Core::Realm
13
+ # @rbs return: void
14
+ def initialize(realm, remote_value)
15
+ @realm = realm
16
+ @remote_value = remote_value
17
+ @disposed = false
18
+ end
19
+
20
+ # Factory method to create JSHandle from remote value
21
+ # @rbs remote_value: Hash[String, untyped]
22
+ # @rbs realm: Core::Realm
23
+ # @rbs return: JSHandle | ElementHandle
24
+ def self.from(remote_value, realm)
25
+ if remote_value['type'] == 'node'
26
+ ElementHandle.new(realm, remote_value)
27
+ else
28
+ new(realm, remote_value)
29
+ end
30
+ end
31
+
32
+ # Get the remote value (BiDi Script.RemoteValue)
33
+ # @rbs return: Hash[String, untyped]
34
+ def remote_value
35
+ @remote_value
36
+ end
37
+
38
+ # Get the remote object (alias for remote_value)
39
+ # @rbs return: Hash[String, untyped]
40
+ def remote_object
41
+ @remote_value
42
+ end
43
+
44
+ # Check if handle has been disposed
45
+ # @rbs return: bool
46
+ def disposed?
47
+ @disposed
48
+ end
49
+
50
+ # Dispose this handle by releasing the remote object
51
+ # @rbs return: void
52
+ def dispose
53
+ return if @disposed
54
+
55
+ @disposed = true
56
+
57
+ # Release the remote reference if it has a handle
58
+ handle_id = id
59
+ @realm.disown([handle_id]).wait if handle_id
60
+ end
61
+
62
+ # Get the handle ID (handle or sharedId)
63
+ # @rbs return: String?
64
+ def id
65
+ @remote_value['handle'] || @remote_value['sharedId']
66
+ end
67
+
68
+ # Evaluate JavaScript function with this handle as the first argument
69
+ # @rbs script: String
70
+ # @rbs *args: untyped
71
+ # @rbs return: untyped
72
+ def evaluate(script, *args)
73
+ assert_not_disposed
74
+
75
+ # Prepend this handle as first argument
76
+ all_args = [@remote_value] + args.map { |arg| Serializer.serialize(arg) }
77
+
78
+ result = @realm.call_function(script, true, arguments: all_args).wait
79
+
80
+ # Check for exceptions
81
+ if result['type'] == 'exception'
82
+ handle_evaluation_exception(result)
83
+ end
84
+
85
+ # Deserialize result
86
+ Deserializer.deserialize(result['result'])
87
+ end
88
+
89
+ # Evaluate JavaScript function and return a handle to the result
90
+ # @rbs script: String
91
+ # @rbs *args: untyped
92
+ # @rbs return: JSHandle
93
+ def evaluate_handle(script, *args)
94
+ assert_not_disposed
95
+
96
+ # Prepend this handle as first argument
97
+ all_args = [@remote_value] + args.map { |arg| Serializer.serialize(arg) }
98
+
99
+ # Puppeteer passes awaitPromise: true to wait for promises to resolve
100
+ result = @realm.call_function(script, true, arguments: all_args).wait
101
+
102
+ # Check for exceptions
103
+ if result['type'] == 'exception'
104
+ handle_evaluation_exception(result)
105
+ end
106
+
107
+ # Return handle (don't deserialize)
108
+ JSHandle.from(result['result'], @realm)
109
+ end
110
+
111
+ # Get a property of the object
112
+ # @rbs property_name: String
113
+ # @rbs return: JSHandle
114
+ def get_property(property_name)
115
+ assert_not_disposed
116
+
117
+ result = @realm.call_function(
118
+ '(object, property) => object[property]',
119
+ false,
120
+ arguments: [
121
+ @remote_value,
122
+ Serializer.serialize(property_name)
123
+ ]
124
+ ).wait
125
+
126
+ if result['type'] == 'exception'
127
+ exception_details = result['exceptionDetails']
128
+ text = exception_details['text'] || 'Evaluation failed'
129
+ raise text
130
+ end
131
+
132
+ JSHandle.from(result['result'], @realm)
133
+ end
134
+
135
+ # Get all properties of the object
136
+ # @rbs return: Hash[String, JSHandle]
137
+ def get_properties
138
+ assert_not_disposed
139
+
140
+ # Get own and inherited properties
141
+ result = @realm.call_function(
142
+ <<~JS,
143
+ (object) => {
144
+ const properties = {};
145
+ let current = object;
146
+
147
+ // Walk the prototype chain
148
+ while (current) {
149
+ const names = Object.getOwnPropertyNames(current);
150
+ for (const name of names) {
151
+ if (!(name in properties)) {
152
+ try {
153
+ properties[name] = object[name];
154
+ } catch (e) {
155
+ // Skip properties that throw on access
156
+ }
157
+ }
158
+ }
159
+ current = Object.getPrototypeOf(current);
160
+ }
161
+
162
+ return properties;
163
+ }
164
+ JS
165
+ false,
166
+ arguments: [@remote_value]
167
+ ).wait
168
+
169
+ if result['type'] == 'exception'
170
+ return {}
171
+ end
172
+
173
+ # Get property entries
174
+ properties_object = result['result']
175
+ return {} if properties_object['type'] == 'undefined' || properties_object['type'] == 'null'
176
+
177
+ # Get each property as a handle
178
+ props_result = @realm.call_function(
179
+ <<~JS,
180
+ (object) => {
181
+ const entries = [];
182
+ for (const key in object) {
183
+ entries.push([key, object[key]]);
184
+ }
185
+ return entries;
186
+ }
187
+ JS
188
+ false,
189
+ arguments: [properties_object]
190
+ ).wait
191
+
192
+ if props_result['type'] == 'exception'
193
+ return {}
194
+ end
195
+
196
+ entries = props_result['result']
197
+ return {} unless entries['type'] == 'array'
198
+
199
+ # Convert to Hash of JSHandles
200
+ result_hash = {}
201
+ entries['value'].each do |entry|
202
+ next unless entry['type'] == 'array'
203
+ next unless entry['value'].length == 2
204
+
205
+ key = Deserializer.deserialize(entry['value'][0])
206
+ value_remote = entry['value'][1]
207
+
208
+ result_hash[key] = JSHandle.from(value_remote, @realm)
209
+ end
210
+
211
+ result_hash
212
+ end
213
+
214
+ # Convert this handle to a JSON-serializable value
215
+ # @rbs return: untyped
216
+ def json_value
217
+ assert_not_disposed
218
+
219
+ # Use evaluate with identity function, just like Puppeteer does
220
+ # This leverages BiDi's built-in serialization with returnByValue: true
221
+ evaluate('(value) => value')
222
+ end
223
+
224
+ # Convert to ElementHandle if this is an element
225
+ # @rbs return: ElementHandle?
226
+ def as_element
227
+ return nil unless @remote_value['type'] == 'node'
228
+
229
+ # Check if it's an element node (nodeType 1) or text node (nodeType 3)
230
+ result = @realm.call_function(
231
+ '(node) => node.nodeType',
232
+ false,
233
+ arguments: [@remote_value]
234
+ ).wait
235
+
236
+ return nil if result['type'] == 'exception'
237
+
238
+ node_type = result['result']['value']
239
+
240
+ # 1 = ELEMENT_NODE, 3 = TEXT_NODE
241
+ if node_type == 1 || node_type == 3
242
+ ElementHandle.new(@realm, @remote_value)
243
+ else
244
+ nil
245
+ end
246
+ end
247
+
248
+ # Check if this is a primitive value
249
+ # @rbs return: bool
250
+ def primitive_value?
251
+ type = @remote_value['type']
252
+ %w[string number bigint boolean undefined null].include?(type)
253
+ end
254
+
255
+ # String representation of this handle
256
+ # @rbs return: String
257
+ def to_s
258
+ return 'JSHandle@disposed' if @disposed
259
+
260
+ if primitive_value?
261
+ value = Deserializer.deserialize(@remote_value)
262
+ # For strings, don't use inspect (no quotes)
263
+ if value.is_a?(String)
264
+ "JSHandle:#{value}"
265
+ else
266
+ "JSHandle:#{value.inspect}"
267
+ end
268
+ else
269
+ "JSHandle@#{@remote_value['type']}"
270
+ end
271
+ end
272
+
273
+ private
274
+
275
+ # Check if this handle has been disposed and raise error if so
276
+ # @rbs return: void
277
+ def assert_not_disposed
278
+ raise JSHandleDisposedError if @disposed
279
+ end
280
+
281
+ # Handle evaluation exceptions
282
+ # @rbs result: Hash[String, untyped]
283
+ # @rbs return: void
284
+ def handle_evaluation_exception(result)
285
+ exception_details = result['exceptionDetails']
286
+ return unless exception_details
287
+
288
+ text = exception_details['text'] || 'Evaluation failed'
289
+ exception = exception_details['exception']
290
+
291
+ error_message = text
292
+
293
+ if exception && exception['type'] != 'error'
294
+ thrown_value = Deserializer.deserialize(exception)
295
+ error_message = "Evaluation failed: #{thrown_value}"
296
+ end
297
+
298
+ raise error_message
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppeteer
4
+ module Bidi
5
+ # Keyboard class for keyboard input operations
6
+ # Based on Puppeteer's BidiKeyboard implementation
7
+ class Keyboard
8
+ def initialize(page, browsing_context)
9
+ @page = page
10
+ @browsing_context = browsing_context
11
+ @pressed_keys = Set.new
12
+ end
13
+
14
+ # Press key down
15
+ # @param key [String] Key name (e.g., 'a', 'Enter', 'ArrowLeft')
16
+ # @param text [String, nil] Text to insert (for CDP compatibility, not used in BiDi)
17
+ # @param commands [Array<String>, nil] Commands to trigger (for CDP compatibility, not used in BiDi)
18
+ def down(key, text: nil, commands: nil)
19
+ # Note: text and commands parameters exist for CDP compatibility but are not used in BiDi
20
+ actions = [{
21
+ type: 'keyDown',
22
+ value: get_bidi_key_value(key)
23
+ }]
24
+
25
+ perform_actions(actions)
26
+ @pressed_keys.add(key)
27
+ end
28
+
29
+ # Release key
30
+ # @param key [String] Key name
31
+ def up(key)
32
+ actions = [{
33
+ type: 'keyUp',
34
+ value: get_bidi_key_value(key)
35
+ }]
36
+
37
+ perform_actions(actions)
38
+ @pressed_keys.delete(key)
39
+ end
40
+
41
+ # Press and release a key
42
+ # @param key [String] Key name
43
+ # @param delay [Numeric, nil] Delay between keydown and keyup in milliseconds
44
+ # @param text [String, nil] Text to insert (for CDP compatibility, not used in BiDi)
45
+ # @param commands [Array<String>, nil] Commands to trigger (for CDP compatibility, not used in BiDi)
46
+ def press(key, delay: nil, text: nil, commands: nil)
47
+ # Note: text and commands parameters exist for CDP compatibility but are not used in BiDi
48
+ actions = [{ type: 'keyDown', value: get_bidi_key_value(key) }]
49
+
50
+ if delay
51
+ actions << {
52
+ type: 'pause',
53
+ duration: delay.to_i
54
+ }
55
+ end
56
+
57
+ actions << { type: 'keyUp', value: get_bidi_key_value(key) }
58
+
59
+ perform_actions(actions)
60
+ end
61
+
62
+ # Type text (types each character with keydown/keyup events)
63
+ # @param text [String] Text to type
64
+ # @param delay [Numeric] Delay between each character in milliseconds
65
+ def type(text, delay: 0)
66
+ actions = []
67
+
68
+ # Split text into individual code points (handles multi-byte Unicode correctly)
69
+ text.each_char do |char|
70
+ key_value = get_bidi_key_value(char)
71
+
72
+ actions << { type: 'keyDown', value: key_value }
73
+ actions << { type: 'keyUp', value: key_value }
74
+
75
+ if delay > 0
76
+ actions << {
77
+ type: 'pause',
78
+ duration: delay.to_i
79
+ }
80
+ end
81
+ end
82
+
83
+ perform_actions(actions)
84
+ end
85
+
86
+ # Send character directly (bypasses keyboard events, uses execCommand)
87
+ # @param char [String] Character to send
88
+ def send_character(char)
89
+ # Validate: cannot send more than 1 character
90
+ # Measures the number of code points rather than UTF-16 code units
91
+ if char.chars.length > 1
92
+ raise ArgumentError, 'Cannot send more than 1 character.'
93
+ end
94
+
95
+ # Get the focused frame (may be an iframe)
96
+ focused_frame = @page.focused_frame
97
+
98
+ # Execute insertText in the focused frame's isolated realm
99
+ focused_frame.isolated_realm.call_function(
100
+ 'function(char) { document.execCommand("insertText", false, char); }',
101
+ false,
102
+ arguments: [{ type: 'string', value: char }]
103
+ )
104
+ end
105
+
106
+ private
107
+
108
+ # Convert key name to BiDi protocol value
109
+ # @param key [String] Key name
110
+ # @return [String] BiDi key value (Unicode character or escape sequence)
111
+ def get_bidi_key_value(key)
112
+ # Normalize line breaks
113
+ normalized_key = case key
114
+ when "\r", "\n" then 'Enter'
115
+ else key
116
+ end
117
+
118
+ # If it's a single character (measured by code points), return as-is
119
+ if normalized_key.length == 1
120
+ return normalized_key
121
+ end
122
+
123
+ # Map key names to WebDriver BiDi Unicode values
124
+ # Reference: https://w3c.github.io/webdriver/#keyboard-actions
125
+ case normalized_key
126
+ # Modifier keys
127
+ when 'Shift', 'ShiftLeft' then "\uE008"
128
+ when 'ShiftRight' then "\uE050"
129
+ when 'Ctrl', 'Control', 'CtrlLeft', 'ControlLeft' then "\uE009"
130
+ when 'CtrlRight', 'ControlRight' then "\uE051"
131
+ when 'Alt', 'AltLeft' then "\uE00A"
132
+ when 'AltRight' then "\uE052"
133
+ when 'Meta', 'MetaLeft' then "\uE03D"
134
+ when 'MetaRight' then "\uE053"
135
+
136
+ when 'CtrlOrMeta', 'ControlOrMeta' then (
137
+ if RUBY_PLATFORM.include?('darwin')
138
+ "\uE03D" # Meta
139
+ else
140
+ "\uE009" # Control
141
+ end
142
+ )
143
+
144
+ # Whitespace keys
145
+ when 'Enter', 'NumpadEnter' then "\uE007"
146
+ when 'Tab' then "\uE004"
147
+ when 'Space' then "\uE00D"
148
+
149
+ # Editing keys
150
+ when 'Backspace' then "\uE003"
151
+ when 'Delete' then "\uE017"
152
+ when 'Escape' then "\uE00C"
153
+
154
+ # Navigation keys
155
+ when 'ArrowUp' then "\uE013"
156
+ when 'ArrowDown' then "\uE015"
157
+ when 'ArrowLeft' then "\uE012"
158
+ when 'ArrowRight' then "\uE014"
159
+ when 'Home' then "\uE011"
160
+ when 'End' then "\uE010"
161
+ when 'PageUp' then "\uE00E"
162
+ when 'PageDown' then "\uE00F"
163
+ when 'Insert' then "\uE016"
164
+
165
+ # Function keys
166
+ when 'F1' then "\uE031"
167
+ when 'F2' then "\uE032"
168
+ when 'F3' then "\uE033"
169
+ when 'F4' then "\uE034"
170
+ when 'F5' then "\uE035"
171
+ when 'F6' then "\uE036"
172
+ when 'F7' then "\uE037"
173
+ when 'F8' then "\uE038"
174
+ when 'F9' then "\uE039"
175
+ when 'F10' then "\uE03A"
176
+ when 'F11' then "\uE03B"
177
+ when 'F12' then "\uE03C"
178
+
179
+ # Numpad
180
+ when 'Numpad0' then "\uE01A"
181
+ when 'Numpad1' then "\uE01B"
182
+ when 'Numpad2' then "\uE01C"
183
+ when 'Numpad3' then "\uE01D"
184
+ when 'Numpad4' then "\uE01E"
185
+ when 'Numpad5' then "\uE01F"
186
+ when 'Numpad6' then "\uE020"
187
+ when 'Numpad7' then "\uE021"
188
+ when 'Numpad8' then "\uE022"
189
+ when 'Numpad9' then "\uE023"
190
+ when 'NumpadMultiply' then "\uE024"
191
+ when 'NumpadAdd' then "\uE025"
192
+ when 'NumpadSubtract' then "\uE027"
193
+ when 'NumpadDecimal' then "\uE028"
194
+ when 'NumpadDivide' then "\uE029"
195
+
196
+ # Key codes - convert to actual characters
197
+ when 'Digit0' then '0'
198
+ when 'Digit1' then '1'
199
+ when 'Digit2' then '2'
200
+ when 'Digit3' then '3'
201
+ when 'Digit4' then '4'
202
+ when 'Digit5' then '5'
203
+ when 'Digit6' then '6'
204
+ when 'Digit7' then '7'
205
+ when 'Digit8' then '8'
206
+ when 'Digit9' then '9'
207
+
208
+ when 'KeyA' then 'a'
209
+ when 'KeyB' then 'b'
210
+ when 'KeyC' then 'c'
211
+ when 'KeyD' then 'd'
212
+ when 'KeyE' then 'e'
213
+ when 'KeyF' then 'f'
214
+ when 'KeyG' then 'g'
215
+ when 'KeyH' then 'h'
216
+ when 'KeyI' then 'i'
217
+ when 'KeyJ' then 'j'
218
+ when 'KeyK' then 'k'
219
+ when 'KeyL' then 'l'
220
+ when 'KeyM' then 'm'
221
+ when 'KeyN' then 'n'
222
+ when 'KeyO' then 'o'
223
+ when 'KeyP' then 'p'
224
+ when 'KeyQ' then 'q'
225
+ when 'KeyR' then 'r'
226
+ when 'KeyS' then 's'
227
+ when 'KeyT' then 't'
228
+ when 'KeyU' then 'u'
229
+ when 'KeyV' then 'v'
230
+ when 'KeyW' then 'w'
231
+ when 'KeyX' then 'x'
232
+ when 'KeyY' then 'y'
233
+ when 'KeyZ' then 'z'
234
+
235
+ # Punctuation
236
+ when 'Semicolon' then ';'
237
+ when 'Equal' then '='
238
+ when 'Comma' then ','
239
+ when 'Minus' then '-'
240
+ when 'Period' then '.'
241
+ when 'Slash' then '/'
242
+ when 'Backquote' then '`'
243
+ when 'BracketLeft' then '['
244
+ when 'Backslash' then '\\'
245
+ when 'BracketRight' then ']'
246
+ when 'Quote' then "'"
247
+
248
+ else
249
+ raise "Unknown key: #{normalized_key.inspect}"
250
+ end
251
+ end
252
+
253
+ # Perform input actions via BiDi
254
+ def perform_actions(action_list)
255
+ @browsing_context.perform_actions([
256
+ {
257
+ type: 'key',
258
+ id: 'default keyboard',
259
+ actions: action_list
260
+ }
261
+ ]).wait
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppeteer
4
+ module Bidi
5
+ # LazyArg defers evaluation of expensive arguments (e.g., handles) until
6
+ # serialization time. Mirrors Puppeteer's LazyArg helper.
7
+ class LazyArg
8
+ def self.create(&block)
9
+ raise ArgumentError, 'LazyArg requires a block' unless block
10
+
11
+ new(&block)
12
+ end
13
+
14
+ def initialize(&block)
15
+ @block = block
16
+ end
17
+
18
+ def resolve
19
+ @block.call
20
+ end
21
+ end
22
+ end
23
+ end