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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CLAUDE/README.md +158 -0
- data/CLAUDE/async_programming.md +158 -0
- data/CLAUDE/click_implementation.md +340 -0
- data/CLAUDE/core_layer_gotchas.md +136 -0
- data/CLAUDE/error_handling.md +232 -0
- data/CLAUDE/file_chooser.md +95 -0
- data/CLAUDE/frame_architecture.md +346 -0
- data/CLAUDE/javascript_evaluation.md +341 -0
- data/CLAUDE/jshandle_implementation.md +505 -0
- data/CLAUDE/keyboard_implementation.md +250 -0
- data/CLAUDE/mouse_implementation.md +140 -0
- data/CLAUDE/navigation_waiting.md +234 -0
- data/CLAUDE/porting_puppeteer.md +214 -0
- data/CLAUDE/query_handler.md +194 -0
- data/CLAUDE/rspec_pending_vs_skip.md +262 -0
- data/CLAUDE/selector_evaluation.md +198 -0
- data/CLAUDE/test_server_routes.md +263 -0
- data/CLAUDE/testing_strategy.md +236 -0
- data/CLAUDE/two_layer_architecture.md +180 -0
- data/CLAUDE/wrapped_element_click.md +247 -0
- data/CLAUDE.md +185 -0
- data/LICENSE.txt +21 -0
- data/README.md +488 -0
- data/Rakefile +21 -0
- data/lib/puppeteer/bidi/async_utils.rb +151 -0
- data/lib/puppeteer/bidi/browser.rb +285 -0
- data/lib/puppeteer/bidi/browser_context.rb +53 -0
- data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
- data/lib/puppeteer/bidi/connection.rb +182 -0
- data/lib/puppeteer/bidi/core/README.md +169 -0
- data/lib/puppeteer/bidi/core/browser.rb +230 -0
- data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
- data/lib/puppeteer/bidi/core/disposable.rb +69 -0
- data/lib/puppeteer/bidi/core/errors.rb +64 -0
- data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
- data/lib/puppeteer/bidi/core/navigation.rb +128 -0
- data/lib/puppeteer/bidi/core/realm.rb +315 -0
- data/lib/puppeteer/bidi/core/request.rb +300 -0
- data/lib/puppeteer/bidi/core/session.rb +153 -0
- data/lib/puppeteer/bidi/core/user_context.rb +208 -0
- data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
- data/lib/puppeteer/bidi/core.rb +45 -0
- data/lib/puppeteer/bidi/deserializer.rb +132 -0
- data/lib/puppeteer/bidi/element_handle.rb +602 -0
- data/lib/puppeteer/bidi/errors.rb +42 -0
- data/lib/puppeteer/bidi/file_chooser.rb +52 -0
- data/lib/puppeteer/bidi/frame.rb +597 -0
- data/lib/puppeteer/bidi/http_response.rb +23 -0
- data/lib/puppeteer/bidi/injected.js +1 -0
- data/lib/puppeteer/bidi/injected_source.rb +21 -0
- data/lib/puppeteer/bidi/js_handle.rb +302 -0
- data/lib/puppeteer/bidi/keyboard.rb +265 -0
- data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
- data/lib/puppeteer/bidi/mouse.rb +170 -0
- data/lib/puppeteer/bidi/page.rb +613 -0
- data/lib/puppeteer/bidi/query_handler.rb +397 -0
- data/lib/puppeteer/bidi/realm.rb +242 -0
- data/lib/puppeteer/bidi/serializer.rb +139 -0
- data/lib/puppeteer/bidi/target.rb +81 -0
- data/lib/puppeteer/bidi/task_manager.rb +44 -0
- data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
- data/lib/puppeteer/bidi/transport.rb +129 -0
- data/lib/puppeteer/bidi/version.rb +7 -0
- data/lib/puppeteer/bidi/wait_task.rb +322 -0
- data/lib/puppeteer/bidi.rb +49 -0
- data/scripts/update_injected_source.rb +57 -0
- data/sig/puppeteer/bidi/browser.rbs +80 -0
- data/sig/puppeteer/bidi/element_handle.rbs +238 -0
- data/sig/puppeteer/bidi/frame.rbs +205 -0
- data/sig/puppeteer/bidi/js_handle.rbs +90 -0
- data/sig/puppeteer/bidi/page.rbs +247 -0
- data/sig/puppeteer/bidi.rbs +15 -0
- 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
|