apparition 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +251 -0
- data/lib/capybara/apparition.rb +20 -0
- data/lib/capybara/apparition/browser.rb +532 -0
- data/lib/capybara/apparition/chrome_client.rb +235 -0
- data/lib/capybara/apparition/command.rb +21 -0
- data/lib/capybara/apparition/cookie.rb +51 -0
- data/lib/capybara/apparition/dev_tools_protocol/session.rb +29 -0
- data/lib/capybara/apparition/dev_tools_protocol/target.rb +52 -0
- data/lib/capybara/apparition/dev_tools_protocol/target_manager.rb +37 -0
- data/lib/capybara/apparition/driver.rb +505 -0
- data/lib/capybara/apparition/errors.rb +230 -0
- data/lib/capybara/apparition/frame.rb +90 -0
- data/lib/capybara/apparition/frame_manager.rb +81 -0
- data/lib/capybara/apparition/inspector.rb +49 -0
- data/lib/capybara/apparition/keyboard.rb +383 -0
- data/lib/capybara/apparition/launcher.rb +218 -0
- data/lib/capybara/apparition/mouse.rb +47 -0
- data/lib/capybara/apparition/network_traffic.rb +9 -0
- data/lib/capybara/apparition/network_traffic/error.rb +12 -0
- data/lib/capybara/apparition/network_traffic/request.rb +47 -0
- data/lib/capybara/apparition/network_traffic/response.rb +49 -0
- data/lib/capybara/apparition/node.rb +844 -0
- data/lib/capybara/apparition/page.rb +711 -0
- data/lib/capybara/apparition/utility.rb +15 -0
- data/lib/capybara/apparition/version.rb +7 -0
- data/lib/capybara/apparition/web_socket_client.rb +80 -0
- metadata +245 -0
@@ -0,0 +1,383 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Apparition
|
4
|
+
class Keyboard
|
5
|
+
attr_reader :modifiers
|
6
|
+
|
7
|
+
def initialize(page)
|
8
|
+
@page = page
|
9
|
+
@modifiers = 0
|
10
|
+
@pressed_keys = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def type(keys)
|
14
|
+
type_with_modifiers(Array(keys))
|
15
|
+
end
|
16
|
+
|
17
|
+
def press(key)
|
18
|
+
if key.is_a? Symbol
|
19
|
+
orig_key = key
|
20
|
+
key = key.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
21
|
+
warn "The use of :#{orig_key} is deprecated, please use :#{key} instead" unless key == orig_key
|
22
|
+
end
|
23
|
+
description = key_description(key)
|
24
|
+
down(description)
|
25
|
+
up(description) if modifier_bit(description.key).zero?
|
26
|
+
end
|
27
|
+
|
28
|
+
def down(description)
|
29
|
+
@modifiers |= modifier_bit(description.key)
|
30
|
+
@pressed_keys[description.key] = description
|
31
|
+
|
32
|
+
@page.command('Input.dispatchKeyEvent',
|
33
|
+
type: 'keyDown',
|
34
|
+
modifiers: @modifiers,
|
35
|
+
windowsVirtualKeyCode: description.keyCode,
|
36
|
+
code: description.code,
|
37
|
+
key: description.key,
|
38
|
+
text: description.text,
|
39
|
+
unmodifiedText: description.text,
|
40
|
+
autoRepeat: false,
|
41
|
+
location: description.location,
|
42
|
+
isKeypad: description.location == 3)
|
43
|
+
end
|
44
|
+
|
45
|
+
def up(description)
|
46
|
+
@modifiers &= ~modifier_bit(description.key)
|
47
|
+
@pressed_keys.delete(description.key)
|
48
|
+
|
49
|
+
@page.command('Input.dispatchKeyEvent',
|
50
|
+
type: 'keyUp',
|
51
|
+
modifiers: @modifiers,
|
52
|
+
key: description.key,
|
53
|
+
windowsVirtualKeyCode: description.keyCode,
|
54
|
+
code: description.code,
|
55
|
+
location: description.location)
|
56
|
+
end
|
57
|
+
|
58
|
+
def yield_with_keys(keys = [])
|
59
|
+
old_pressed_keys = @pressed_keys
|
60
|
+
@pressed_keys = {}
|
61
|
+
keys.each do |key|
|
62
|
+
press key
|
63
|
+
end
|
64
|
+
yield
|
65
|
+
release_pressed_keys
|
66
|
+
@pressed_keys = old_pressed_keys
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def type_with_modifiers(keys)
|
72
|
+
keys = Array(keys)
|
73
|
+
old_pressed_keys = @pressed_keys
|
74
|
+
@pressed_keys = {}
|
75
|
+
|
76
|
+
keys.each do |sequence|
|
77
|
+
if sequence.is_a? Array
|
78
|
+
type_with_modifiers(sequence)
|
79
|
+
elsif sequence.is_a? String
|
80
|
+
sequence.each_char { |char| press char }
|
81
|
+
else
|
82
|
+
press sequence
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
release_pressed_keys
|
87
|
+
@pressed_keys = old_pressed_keys
|
88
|
+
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def release_pressed_keys
|
93
|
+
@pressed_keys.values.each { |desc| up(desc) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def key_description(key)
|
97
|
+
shift = (@modifiers & 8).nonzero?
|
98
|
+
description = OpenStruct.new(
|
99
|
+
key: '',
|
100
|
+
keyCode: 0,
|
101
|
+
code: '',
|
102
|
+
text: '',
|
103
|
+
location: 0
|
104
|
+
)
|
105
|
+
|
106
|
+
definition = KEY_DEFINITIONS[key.to_sym]
|
107
|
+
raise KeyError, "Unknown key: #{key}" if definition.nil?
|
108
|
+
|
109
|
+
definition = OpenStruct.new definition
|
110
|
+
|
111
|
+
description.key = definition.key if definition.key
|
112
|
+
description.key = definition.shiftKey if shift && definition.shiftKey
|
113
|
+
|
114
|
+
description.keyCode = definition.keyCode if definition.keyCode
|
115
|
+
description.keyCode = definition.shiftKeyCode if shift && definition.shiftKeyCode
|
116
|
+
|
117
|
+
description.code = definition.code if definition.code
|
118
|
+
|
119
|
+
description.location = definition.location if definition.location
|
120
|
+
|
121
|
+
description.text = description.key if description.key.length == 1
|
122
|
+
description.text = definition.text if definition.text
|
123
|
+
description.text = definition.shiftText if shift && definition.shiftText
|
124
|
+
|
125
|
+
# if any modifiers besides shift are pressed, no text should be sent
|
126
|
+
description.text = '' if (@modifiers & ~8).nonzero?
|
127
|
+
|
128
|
+
description
|
129
|
+
end
|
130
|
+
|
131
|
+
def modifier_bit(key)
|
132
|
+
case key
|
133
|
+
when 'Alt' then 1
|
134
|
+
when 'Control' then 2
|
135
|
+
when 'Meta' then 4
|
136
|
+
when 'Shift' then 8
|
137
|
+
else
|
138
|
+
0
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# /**
|
143
|
+
# * @typedef {Object} KeyDefinition
|
144
|
+
# * @property {number=} keyCode
|
145
|
+
# * @property {number=} shiftKeyCode
|
146
|
+
# * @property {string=} key
|
147
|
+
# * @property {string=} shiftKey
|
148
|
+
# * @property {string=} code
|
149
|
+
# * @property {string=} text
|
150
|
+
# * @property {string=} shiftText
|
151
|
+
# * @property {number=} location
|
152
|
+
# */
|
153
|
+
|
154
|
+
# rubocop:disable Metrics/LineLength
|
155
|
+
KEY_DEFINITIONS = {
|
156
|
+
'0': { 'keyCode': 48, 'key': '0', 'code': 'Digit0' },
|
157
|
+
'1': { 'keyCode': 49, 'key': '1', 'code': 'Digit1' },
|
158
|
+
'2': { 'keyCode': 50, 'key': '2', 'code': 'Digit2' },
|
159
|
+
'3': { 'keyCode': 51, 'key': '3', 'code': 'Digit3' },
|
160
|
+
'4': { 'keyCode': 52, 'key': '4', 'code': 'Digit4' },
|
161
|
+
'5': { 'keyCode': 53, 'key': '5', 'code': 'Digit5' },
|
162
|
+
'6': { 'keyCode': 54, 'key': '6', 'code': 'Digit6' },
|
163
|
+
'7': { 'keyCode': 55, 'key': '7', 'code': 'Digit7' },
|
164
|
+
'8': { 'keyCode': 56, 'key': '8', 'code': 'Digit8' },
|
165
|
+
'9': { 'keyCode': 57, 'key': '9', 'code': 'Digit9' },
|
166
|
+
'power': { 'key': 'Power', 'code': 'Power' },
|
167
|
+
'eject': { 'key': 'Eject', 'code': 'Eject' },
|
168
|
+
'abort': { 'keyCode': 3, 'code': 'Abort', 'key': 'Cancel' },
|
169
|
+
'help': { 'keyCode': 6, 'code': 'Help', 'key': 'Help' },
|
170
|
+
'backspace': { 'keyCode': 8, 'code': 'Backspace', 'key': 'Backspace' },
|
171
|
+
'tab': { 'keyCode': 9, 'code': 'Tab', 'key': 'Tab' },
|
172
|
+
'numpad5': { 'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3 },
|
173
|
+
'numpad_enter': { 'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': "\r", 'location': 3 },
|
174
|
+
'enter': { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" },
|
175
|
+
"\r": { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" },
|
176
|
+
"\n": { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" },
|
177
|
+
'shift_left': { 'keyCode': 16, 'code': 'ShiftLeft', 'key': 'Shift', 'location': 1 },
|
178
|
+
'shift_right': { 'keyCode': 16, 'code': 'ShiftRight', 'key': 'Shift', 'location': 2 },
|
179
|
+
'control_left': { 'keyCode': 17, 'code': 'ControlLeft', 'key': 'Control', 'location': 1 },
|
180
|
+
'control_right': { 'keyCode': 17, 'code': 'ControlRight', 'key': 'Control', 'location': 2 },
|
181
|
+
'alt_left': { 'keyCode': 18, 'code': 'AltLeft', 'key': 'Alt', 'location': 1 },
|
182
|
+
'alt_right': { 'keyCode': 18, 'code': 'AltRight', 'key': 'Alt', 'location': 2 },
|
183
|
+
'pause': { 'keyCode': 19, 'code': 'Pause', 'key': 'Pause' },
|
184
|
+
'caps_lock': { 'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock' },
|
185
|
+
'escape': { 'keyCode': 27, 'code': 'Escape', 'key': 'Escape' },
|
186
|
+
'convert': { 'keyCode': 28, 'code': 'Convert', 'key': 'Convert' },
|
187
|
+
'non_convert': { 'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert' },
|
188
|
+
'space': { 'keyCode': 32, 'code': 'Space', 'key': ' ' },
|
189
|
+
'numpad9': { 'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'code': 'Numpad9', 'shiftKey': '9', 'location': 3 },
|
190
|
+
'page_up': { 'keyCode': 33, 'code': 'PageUp', 'key': 'PageUp' },
|
191
|
+
'numpad3': { 'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'code': 'Numpad3', 'shiftKey': '3', 'location': 3 },
|
192
|
+
'page_down': { 'keyCode': 34, 'code': 'PageDown', 'key': 'PageDown' },
|
193
|
+
'end': { 'keyCode': 35, 'code': 'End', 'key': 'End' },
|
194
|
+
'numpad1': { 'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3 },
|
195
|
+
'home': { 'keyCode': 36, 'code': 'Home', 'key': 'Home' },
|
196
|
+
'numpad7': { 'keyCode': 36, 'shiftKeyCode': 103, 'key': 'Home', 'code': 'Numpad7', 'shiftKey': '7', 'location': 3 },
|
197
|
+
'left': { 'keyCode': 37, 'code': 'ArrowLeft', 'key': 'ArrowLeft' },
|
198
|
+
'numpad4': { 'keyCode': 37, 'shiftKeyCode': 100, 'key': 'ArrowLeft', 'code': 'Numpad4', 'shiftKey': '4', 'location': 3 },
|
199
|
+
'numpad8': { 'keyCode': 38, 'shiftKeyCode': 104, 'key': 'ArrowUp', 'code': 'Numpad8', 'shiftKey': '8', 'location': 3 },
|
200
|
+
'up': { 'keyCode': 38, 'code': 'ArrowUp', 'key': 'ArrowUp' },
|
201
|
+
'right': { 'keyCode': 39, 'code': 'ArrowRight', 'key': 'ArrowRight' },
|
202
|
+
'numpad6': { 'keyCode': 39, 'shiftKeyCode': 102, 'key': 'ArrowRight', 'code': 'Numpad6', 'shiftKey': '6', 'location': 3 },
|
203
|
+
'numpad2': { 'keyCode': 40, 'shiftKeyCode': 98, 'key': 'ArrowDown', 'code': 'Numpad2', 'shiftKey': '2', 'location': 3 },
|
204
|
+
'down': { 'keyCode': 40, 'code': 'ArrowDown', 'key': 'ArrowDown' },
|
205
|
+
'select': { 'keyCode': 41, 'code': 'Select', 'key': 'Select' },
|
206
|
+
'open': { 'keyCode': 43, 'code': 'Open', 'key': 'Execute' },
|
207
|
+
'print_screen': { 'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen' },
|
208
|
+
'insert': { 'keyCode': 45, 'code': 'Insert', 'key': 'Insert' },
|
209
|
+
'numpad0': { 'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3 },
|
210
|
+
'delete': { 'keyCode': 46, 'code': 'Delete', 'key': 'Delete' },
|
211
|
+
'decimal': { 'keyCode': 46, 'shiftKeyCode': 110, 'code': 'NumpadDecimal', 'key': "\u0000", 'shiftKey': '.', 'location': 3 },
|
212
|
+
'digit0': { 'keyCode': 48, 'code': 'Digit0', 'shiftKey': ')', 'key': '0' },
|
213
|
+
'digit1': { 'keyCode': 49, 'code': 'Digit1', 'shiftKey': '!', 'key': '1' },
|
214
|
+
'digit2': { 'keyCode': 50, 'code': 'Digit2', 'shiftKey': '@', 'key': '2' },
|
215
|
+
'digit3': { 'keyCode': 51, 'code': 'Digit3', 'shiftKey': '#', 'key': '3' },
|
216
|
+
'digit4': { 'keyCode': 52, 'code': 'Digit4', 'shiftKey': '$', 'key': '4' },
|
217
|
+
'digit5': { 'keyCode': 53, 'code': 'Digit5', 'shiftKey': '%', 'key': '5' },
|
218
|
+
'digit6': { 'keyCode': 54, 'code': 'Digit6', 'shiftKey': '^', 'key': '6' },
|
219
|
+
'digit7': { 'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7' },
|
220
|
+
'digit8': { 'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8' },
|
221
|
+
'digit9': { 'keyCode': 57, 'code': 'Digit9', 'shiftKey': "\(", 'key': '9' },
|
222
|
+
'meta_left': { 'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta' },
|
223
|
+
'meta_right': { 'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta' },
|
224
|
+
'context_menu': { 'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu' },
|
225
|
+
'multiply': { 'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3 },
|
226
|
+
'add': { 'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3 },
|
227
|
+
'subtract': { 'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3 },
|
228
|
+
'divide': { 'keyCode': 111, 'code': 'NumpadDivide', 'key': '/', 'location': 3 },
|
229
|
+
'F1': { 'keyCode': 112, 'code': 'F1', 'key': 'F1' },
|
230
|
+
'f2': { 'keyCode': 113, 'code': 'F2', 'key': 'F2' },
|
231
|
+
'f3': { 'keyCode': 114, 'code': 'F3', 'key': 'F3' },
|
232
|
+
'f4': { 'keyCode': 115, 'code': 'F4', 'key': 'F4' },
|
233
|
+
'f5': { 'keyCode': 116, 'code': 'F5', 'key': 'F5' },
|
234
|
+
'f6': { 'keyCode': 117, 'code': 'F6', 'key': 'F6' },
|
235
|
+
'f7': { 'keyCode': 118, 'code': 'F7', 'key': 'F7' },
|
236
|
+
'f8': { 'keyCode': 119, 'code': 'F8', 'key': 'F8' },
|
237
|
+
'f9': { 'keyCode': 120, 'code': 'F9', 'key': 'F9' },
|
238
|
+
'f10': { 'keyCode': 121, 'code': 'F10', 'key': 'F10' },
|
239
|
+
'f11': { 'keyCode': 122, 'code': 'F11', 'key': 'F11' },
|
240
|
+
'f12': { 'keyCode': 123, 'code': 'F12', 'key': 'F12' },
|
241
|
+
'f13': { 'keyCode': 124, 'code': 'F13', 'key': 'F13' },
|
242
|
+
'f14': { 'keyCode': 125, 'code': 'F14', 'key': 'F14' },
|
243
|
+
'f15': { 'keyCode': 126, 'code': 'F15', 'key': 'F15' },
|
244
|
+
'f16': { 'keyCode': 127, 'code': 'F16', 'key': 'F16' },
|
245
|
+
'f17': { 'keyCode': 128, 'code': 'F17', 'key': 'F17' },
|
246
|
+
'f18': { 'keyCode': 129, 'code': 'F18', 'key': 'F18' },
|
247
|
+
'f19': { 'keyCode': 130, 'code': 'F19', 'key': 'F19' },
|
248
|
+
'f20': { 'keyCode': 131, 'code': 'F20', 'key': 'F20' },
|
249
|
+
'f21': { 'keyCode': 132, 'code': 'F21', 'key': 'F21' },
|
250
|
+
'f22': { 'keyCode': 133, 'code': 'F22', 'key': 'F22' },
|
251
|
+
'f23': { 'keyCode': 134, 'code': 'F23', 'key': 'F23' },
|
252
|
+
'f24': { 'keyCode': 135, 'code': 'F24', 'key': 'F24' },
|
253
|
+
'num_lock': { 'keyCode': 144, 'code': 'NumLock', 'key': 'NumLock' },
|
254
|
+
'scroll_lock': { 'keyCode': 145, 'code': 'ScrollLock', 'key': 'ScrollLock' },
|
255
|
+
'audio_volume_mute': { 'keyCode': 173, 'code': 'AudioVolumeMute', 'key': 'AudioVolumeMute' },
|
256
|
+
'audio_volume_down': { 'keyCode': 174, 'code': 'AudioVolumeDown', 'key': 'AudioVolumeDown' },
|
257
|
+
'audio_volume_up': { 'keyCode': 175, 'code': 'AudioVolumeUp', 'key': 'AudioVolumeUp' },
|
258
|
+
'media_track_next': { 'keyCode': 176, 'code': 'MediaTrackNext', 'key': 'MediaTrackNext' },
|
259
|
+
'media_track_previous': { 'keyCode': 177, 'code': 'MediaTrackPrevious', 'key': 'MediaTrackPrevious' },
|
260
|
+
'media_stop': { 'keyCode': 178, 'code': 'MediaStop', 'key': 'MediaStop' },
|
261
|
+
'media_play_pause': { 'keyCode': 179, 'code': 'MediaPlayPause', 'key': 'MediaPlayPause' },
|
262
|
+
'semicolon': { 'keyCode': 186, 'code': 'Semicolon', 'shiftKey': ':', 'key': ';' },
|
263
|
+
'equals': { 'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '=' },
|
264
|
+
'equal': { 'keyCode': 187, 'code': 'NumpadEqual', 'key': '=', 'location': 3 },
|
265
|
+
'comma': { 'keyCode': 188, 'code': 'Comma', 'shiftKey': "\<", 'key': ',' },
|
266
|
+
'minus': { 'keyCode': 189, 'code': 'Minus', 'shiftKey': '_', 'key': '-' },
|
267
|
+
'period': { 'keyCode': 190, 'code': 'Period', 'shiftKey': '>', 'key': '.' },
|
268
|
+
'slash': { 'keyCode': 191, 'code': 'Slash', 'shiftKey': '?', 'key': '/' },
|
269
|
+
'backquote': { 'keyCode': 192, 'code': 'Backquote', 'shiftKey': '~', 'key': '`' },
|
270
|
+
'bracket_left': { 'keyCode': 219, 'code': 'BracketLeft', 'shiftKey': '{', 'key': '[' },
|
271
|
+
'backslash': { 'keyCode': 220, 'code': 'Backslash', 'shiftKey': '|', 'key': '\\' },
|
272
|
+
'bracket_right': { 'keyCode': 221, 'code': 'BracketRight', 'shiftKey': '}', 'key': ']' },
|
273
|
+
'quote': { 'keyCode': 222, 'code': 'Quote', 'shiftKey': '"', 'key': "'" },
|
274
|
+
'alt_graph': { 'keyCode': 225, 'code': 'AltGraph', 'key': 'AltGraph' },
|
275
|
+
'props': { 'keyCode': 247, 'code': 'Props', 'key': 'CrSel' },
|
276
|
+
'cancel': { 'keyCode': 3, 'key': 'Cancel', 'code': 'Abort' },
|
277
|
+
'clear': { 'keyCode': 12, 'key': 'Clear', 'code': 'Numpad5', 'location': 3 },
|
278
|
+
'shift': { 'keyCode': 16, 'key': 'Shift', 'code': 'ShiftLeft' },
|
279
|
+
'control': { 'keyCode': 17, 'key': 'Control', 'code': 'ControlLeft' },
|
280
|
+
'ctrl': { 'keyCode': 17, 'key': 'Control', 'code': 'ControlLeft' },
|
281
|
+
'alt': { 'keyCode': 18, 'key': 'Alt', 'code': 'AltLeft' },
|
282
|
+
'accept': { 'keyCode': 30, 'key': 'Accept' },
|
283
|
+
'mode_change': { 'keyCode': 31, 'key': 'ModeChange' },
|
284
|
+
' ': { 'keyCode': 32, 'key': ' ', 'code': 'Space' },
|
285
|
+
'print': { 'keyCode': 42, 'key': 'Print' },
|
286
|
+
'execute': { 'keyCode': 43, 'key': 'Execute', 'code': 'Open' },
|
287
|
+
"\u0000": { 'keyCode': 46, 'key': "\u0000", 'code': 'NumpadDecimal', 'location': 3 },
|
288
|
+
'a': { 'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a' },
|
289
|
+
'b': { 'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b' },
|
290
|
+
'c': { 'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c' },
|
291
|
+
'd': { 'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd' },
|
292
|
+
'e': { 'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e' },
|
293
|
+
'f': { 'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f' },
|
294
|
+
'g': { 'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g' },
|
295
|
+
'h': { 'keyCode': 72, 'code': 'KeyH', 'shiftKey': 'H', 'key': 'h' },
|
296
|
+
'i': { 'keyCode': 73, 'code': 'KeyI', 'shiftKey': 'I', 'key': 'i' },
|
297
|
+
'j': { 'keyCode': 74, 'code': 'KeyJ', 'shiftKey': 'J', 'key': 'j' },
|
298
|
+
'k': { 'keyCode': 75, 'code': 'KeyK', 'shiftKey': 'K', 'key': 'k' },
|
299
|
+
'l': { 'keyCode': 76, 'code': 'KeyL', 'shiftKey': 'L', 'key': 'l' },
|
300
|
+
'm': { 'keyCode': 77, 'code': 'KeyM', 'shiftKey': 'M', 'key': 'm' },
|
301
|
+
'n': { 'keyCode': 78, 'code': 'KeyN', 'shiftKey': 'N', 'key': 'n' },
|
302
|
+
'o': { 'keyCode': 79, 'code': 'KeyO', 'shiftKey': 'O', 'key': 'o' },
|
303
|
+
'p': { 'keyCode': 80, 'code': 'KeyP', 'shiftKey': 'P', 'key': 'p' },
|
304
|
+
'q': { 'keyCode': 81, 'code': 'KeyQ', 'shiftKey': 'Q', 'key': 'q' },
|
305
|
+
'r': { 'keyCode': 82, 'code': 'KeyR', 'shiftKey': 'R', 'key': 'r' },
|
306
|
+
's': { 'keyCode': 83, 'code': 'KeyS', 'shiftKey': 'S', 'key': 's' },
|
307
|
+
't': { 'keyCode': 84, 'code': 'KeyT', 'shiftKey': 'T', 'key': 't' },
|
308
|
+
'u': { 'keyCode': 85, 'code': 'KeyU', 'shiftKey': 'U', 'key': 'u' },
|
309
|
+
'v': { 'keyCode': 86, 'code': 'KeyV', 'shiftKey': 'V', 'key': 'v' },
|
310
|
+
'w': { 'keyCode': 87, 'code': 'KeyW', 'shiftKey': 'W', 'key': 'w' },
|
311
|
+
'x': { 'keyCode': 88, 'code': 'KeyX', 'shiftKey': 'X', 'key': 'x' },
|
312
|
+
'y': { 'keyCode': 89, 'code': 'KeyY', 'shiftKey': 'Y', 'key': 'y' },
|
313
|
+
'z': { 'keyCode': 90, 'code': 'KeyZ', 'shiftKey': 'Z', 'key': 'z' },
|
314
|
+
'meta': { 'keyCode': 91, 'key': 'Meta', 'code': 'MetaLeft' },
|
315
|
+
'command': { 'keyCode': 91, 'key': 'Meta', 'code': 'MetaLeft' },
|
316
|
+
'*': { 'keyCode': 106, 'key': '*', 'code': 'NumpadMultiply', 'location': 3 },
|
317
|
+
'+': { 'keyCode': 107, 'key': '+', 'code': 'NumpadAdd', 'location': 3 },
|
318
|
+
'-': { 'keyCode': 109, 'key': '-', 'code': 'NumpadSubtract', 'location': 3 },
|
319
|
+
'/': { 'keyCode': 111, 'key': '/', 'code': 'NumpadDivide', 'location': 3 },
|
320
|
+
';': { 'keyCode': 186, 'key': ';', 'code': 'Semicolon' },
|
321
|
+
'=': { 'keyCode': 187, 'key': '=', 'code': 'Equal' },
|
322
|
+
',': { 'keyCode': 188, 'key': ',', 'code': 'Comma' },
|
323
|
+
'.': { 'keyCode': 190, 'key': '.', 'code': 'Period' },
|
324
|
+
'`': { 'keyCode': 192, 'key': '`', 'code': 'Backquote' },
|
325
|
+
'[': { 'keyCode': 219, 'key': '[', 'code': 'BracketLeft' },
|
326
|
+
'\\': { 'keyCode': 220, 'key': '\\', 'code': 'Backslash' },
|
327
|
+
']': { 'keyCode': 221, 'key': ']', 'code': 'BracketRight' },
|
328
|
+
'\'': { 'keyCode': 222, 'key': '\'', 'code': 'Quote' },
|
329
|
+
'attn': { 'keyCode': 246, 'key': 'Attn' },
|
330
|
+
'cr_sel': { 'keyCode': 247, 'key': 'CrSel', 'code': 'Props' },
|
331
|
+
'ex_sel': { 'keyCode': 248, 'key': 'ExSel' },
|
332
|
+
'erase_eof': { 'keyCode': 249, 'key': 'EraseEof' },
|
333
|
+
'play': { 'keyCode': 250, 'key': 'Play' },
|
334
|
+
'zoom_out': { 'keyCode': 251, 'key': 'ZoomOut' },
|
335
|
+
')': { 'keyCode': 48, 'key': ')', 'code': 'Digit0' },
|
336
|
+
'!': { 'keyCode': 49, 'key': '!', 'code': 'Digit1' },
|
337
|
+
'@': { 'keyCode': 50, 'key': '@', 'code': 'Digit2' },
|
338
|
+
'#': { 'keyCode': 51, 'key': '#', 'code': 'Digit3' },
|
339
|
+
'$': { 'keyCode': 52, 'key': '$', 'code': 'Digit4' },
|
340
|
+
'%': { 'keyCode': 53, 'key': '%', 'code': 'Digit5' },
|
341
|
+
'^': { 'keyCode': 54, 'key': '^', 'code': 'Digit6' },
|
342
|
+
'&': { 'keyCode': 55, 'key': '&', 'code': 'Digit7' },
|
343
|
+
'(': { 'keyCode': 57, 'key': "\(", 'code': 'Digit9' },
|
344
|
+
'A': { 'keyCode': 65, 'key': 'A', 'code': 'KeyA' },
|
345
|
+
'B': { 'keyCode': 66, 'key': 'B', 'code': 'KeyB' },
|
346
|
+
'C': { 'keyCode': 67, 'key': 'C', 'code': 'KeyC' },
|
347
|
+
'D': { 'keyCode': 68, 'key': 'D', 'code': 'KeyD' },
|
348
|
+
'E': { 'keyCode': 69, 'key': 'E', 'code': 'KeyE' },
|
349
|
+
'F': { 'keyCode': 70, 'key': 'F', 'code': 'KeyF' },
|
350
|
+
'G': { 'keyCode': 71, 'key': 'G', 'code': 'KeyG' },
|
351
|
+
'H': { 'keyCode': 72, 'key': 'H', 'code': 'KeyH' },
|
352
|
+
'I': { 'keyCode': 73, 'key': 'I', 'code': 'KeyI' },
|
353
|
+
'J': { 'keyCode': 74, 'key': 'J', 'code': 'KeyJ' },
|
354
|
+
'K': { 'keyCode': 75, 'key': 'K', 'code': 'KeyK' },
|
355
|
+
'L': { 'keyCode': 76, 'key': 'L', 'code': 'KeyL' },
|
356
|
+
'M': { 'keyCode': 77, 'key': 'M', 'code': 'KeyM' },
|
357
|
+
'N': { 'keyCode': 78, 'key': 'N', 'code': 'KeyN' },
|
358
|
+
'O': { 'keyCode': 79, 'key': 'O', 'code': 'KeyO' },
|
359
|
+
'P': { 'keyCode': 80, 'key': 'P', 'code': 'KeyP' },
|
360
|
+
'Q': { 'keyCode': 81, 'key': 'Q', 'code': 'KeyQ' },
|
361
|
+
'R': { 'keyCode': 82, 'key': 'R', 'code': 'KeyR' },
|
362
|
+
'S': { 'keyCode': 83, 'key': 'S', 'code': 'KeyS' },
|
363
|
+
'T': { 'keyCode': 84, 'key': 'T', 'code': 'KeyT' },
|
364
|
+
'U': { 'keyCode': 85, 'key': 'U', 'code': 'KeyU' },
|
365
|
+
'V': { 'keyCode': 86, 'key': 'V', 'code': 'KeyV' },
|
366
|
+
'W': { 'keyCode': 87, 'key': 'W', 'code': 'KeyW' },
|
367
|
+
'X': { 'keyCode': 88, 'key': 'X', 'code': 'KeyX' },
|
368
|
+
'Y': { 'keyCode': 89, 'key': 'Y', 'code': 'KeyY' },
|
369
|
+
'Z': { 'keyCode': 90, 'key': 'Z', 'code': 'KeyZ' },
|
370
|
+
':': { 'keyCode': 186, 'key': ':', 'code': 'Semicolon' },
|
371
|
+
'<': { 'keyCode': 188, 'key': "\<", 'code': 'Comma' },
|
372
|
+
'_': { 'keyCode': 189, 'key': '_', 'code': 'Minus' },
|
373
|
+
'>': { 'keyCode': 190, 'key': '>', 'code': 'Period' },
|
374
|
+
'?': { 'keyCode': 191, 'key': '?', 'code': 'Slash' },
|
375
|
+
'~': { 'keyCode': 192, 'key': '~', 'code': 'Backquote' },
|
376
|
+
'{': { 'keyCode': 219, 'key': '{', 'code': 'BracketLeft' },
|
377
|
+
'|': { 'keyCode': 220, 'key': '|', 'code': 'Backslash' },
|
378
|
+
'}': { 'keyCode': 221, 'key': '}', 'code': 'BracketRight' },
|
379
|
+
'"': { 'keyCode': 222, 'key': '"', 'code': 'Quote' }
|
380
|
+
}.freeze
|
381
|
+
# rubocop:enable Metrics/LineLength
|
382
|
+
end
|
383
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Apparition
|
4
|
+
class Browser
|
5
|
+
class Launcher
|
6
|
+
KILL_TIMEOUT = 2
|
7
|
+
|
8
|
+
BROWSER_HOST = '127.0.0.1'
|
9
|
+
BROWSER_PORT = '0'
|
10
|
+
|
11
|
+
# Chromium command line options
|
12
|
+
# https://peter.sh/experiments/chromium-command-line-switches/
|
13
|
+
DEFAULT_OPTIONS = {
|
14
|
+
'disable-background-networking' => nil,
|
15
|
+
'disable-background-timer-throttling' => nil,
|
16
|
+
'disable-breakpad' => nil,
|
17
|
+
'disable-client-side-phishing-detection' => nil,
|
18
|
+
'disable-default-apps' => nil,
|
19
|
+
'disable-dev-shm-usage' => nil,
|
20
|
+
'disable-extensions' => nil,
|
21
|
+
'disable-features=site-per-process' => nil,
|
22
|
+
'disable-hang-monitor' => nil,
|
23
|
+
'disable-popup-blocking' => nil,
|
24
|
+
'disable-prompt-on-repost' => nil,
|
25
|
+
'disable-sync' => nil,
|
26
|
+
'disable-translate' => nil,
|
27
|
+
'metrics-recording-only' => nil,
|
28
|
+
'no-first-run' => nil,
|
29
|
+
'safebrowsing-disable-auto-update' => nil,
|
30
|
+
'enable-automation' => nil,
|
31
|
+
'password-store=basic' => nil,
|
32
|
+
'use-mock-keychain' => nil,
|
33
|
+
'keep-alive-for-test' => nil,
|
34
|
+
'window-size' => '1024,768',
|
35
|
+
'homepage' => 'about:blank',
|
36
|
+
# Note: --no-sandbox is not needed if you properly setup a user in the container.
|
37
|
+
# https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
|
38
|
+
# "no-sandbox" => nil,
|
39
|
+
# "disable-web-security" => nil,
|
40
|
+
'remote-debugging-port' => BROWSER_PORT,
|
41
|
+
'remote-debugging-address' => BROWSER_HOST
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
HEADLESS_OPTIONS = {
|
45
|
+
'headless' => nil,
|
46
|
+
'hide-scrollbars' => nil,
|
47
|
+
'mute-audio' => nil
|
48
|
+
}
|
49
|
+
|
50
|
+
def self.start(*args)
|
51
|
+
new(*args).tap(&:start)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.process_killer(pid)
|
55
|
+
proc do
|
56
|
+
begin
|
57
|
+
if Capybara::Apparition.windows?
|
58
|
+
::Process.kill('KILL', pid)
|
59
|
+
else
|
60
|
+
::Process.kill('TERM', pid)
|
61
|
+
start = Time.now
|
62
|
+
while ::Process.wait(pid, ::Process::WNOHANG).nil?
|
63
|
+
sleep 0.05
|
64
|
+
next unless (Time.now - start) > KILL_TIMEOUT
|
65
|
+
|
66
|
+
::Process.kill('KILL', pid)
|
67
|
+
::Process.wait(pid)
|
68
|
+
break
|
69
|
+
end
|
70
|
+
end
|
71
|
+
rescue Errno::ESRCH, Errno::ECHILD # rubocop:disable Lint/HandleExceptions
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(headless:, **options)
|
77
|
+
@path = ENV['BROWSER_PATH']
|
78
|
+
@options = DEFAULT_OPTIONS.merge(options.fetch(:browser, {}))
|
79
|
+
if headless
|
80
|
+
@options.merge!(HEADLESS_OPTIONS)
|
81
|
+
@options['disable-gpu'] = nil if Capybara::Apparition.windows?
|
82
|
+
end
|
83
|
+
@options['user-data-dir'] = Dir.mktmpdir
|
84
|
+
end
|
85
|
+
|
86
|
+
def start
|
87
|
+
@output = Queue.new
|
88
|
+
@read_io, @write_io = IO.pipe
|
89
|
+
|
90
|
+
@out_thread = Thread.new do
|
91
|
+
while !@read_io.eof? && (data = @read_io.readpartial(512))
|
92
|
+
@output << data
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
process_options = { in: File::NULL }
|
97
|
+
process_options[:pgroup] = true unless Capybara::Apparition.windows?
|
98
|
+
process_options[:out] = process_options[:err] = @write_io if Capybara::Apparition.mri?
|
99
|
+
|
100
|
+
redirect_stdout do
|
101
|
+
cmd = [path] + @options.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
|
102
|
+
@pid = ::Process.spawn(*cmd, process_options)
|
103
|
+
ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
|
104
|
+
end
|
105
|
+
|
106
|
+
sleep 3
|
107
|
+
end
|
108
|
+
|
109
|
+
def stop
|
110
|
+
return unless @pid
|
111
|
+
|
112
|
+
kill
|
113
|
+
ObjectSpace.undefine_finalizer(self)
|
114
|
+
end
|
115
|
+
|
116
|
+
def restart
|
117
|
+
stop
|
118
|
+
start
|
119
|
+
end
|
120
|
+
|
121
|
+
def host
|
122
|
+
@host ||= ws_url.host
|
123
|
+
end
|
124
|
+
|
125
|
+
def port
|
126
|
+
@port ||= ws_url.port
|
127
|
+
end
|
128
|
+
|
129
|
+
def ws_url
|
130
|
+
@ws_url ||= begin
|
131
|
+
regexp = %r{DevTools listening on (ws://.*)}
|
132
|
+
url = nil
|
133
|
+
loop do
|
134
|
+
break if (url = @output.pop.scan(regexp)[0])
|
135
|
+
end
|
136
|
+
@out_thread.kill
|
137
|
+
close_io
|
138
|
+
Addressable::URI.parse(url[0])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def redirect_stdout
|
145
|
+
if Capybara::Apparition.mri?
|
146
|
+
yield
|
147
|
+
else
|
148
|
+
begin
|
149
|
+
prev = STDOUT.dup
|
150
|
+
$stdout = @write_io
|
151
|
+
STDOUT.reopen(@write_io)
|
152
|
+
yield
|
153
|
+
ensure
|
154
|
+
STDOUT.reopen(prev)
|
155
|
+
$stdout = STDOUT
|
156
|
+
prev.close
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def kill
|
162
|
+
self.class.process_killer(@pid).call
|
163
|
+
@pid = nil
|
164
|
+
end
|
165
|
+
|
166
|
+
def close_io
|
167
|
+
[@write_io, @read_io].each do |io|
|
168
|
+
begin
|
169
|
+
io.close unless io.closed?
|
170
|
+
rescue IOError
|
171
|
+
raise unless RUBY_ENGINE == 'jruby'
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def path
|
177
|
+
host_os = RbConfig::CONFIG['host_os']
|
178
|
+
@path ||= case RbConfig::CONFIG['host_os']
|
179
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
180
|
+
windows_path
|
181
|
+
when /darwin|mac os/
|
182
|
+
macosx_path
|
183
|
+
when /linux|solaris|bsd/
|
184
|
+
find_first_binary('google-chrome', 'chrome') || '/usr/bin/chrome'
|
185
|
+
else
|
186
|
+
raise ArgumentError, "unknown os: #{host_os.inspect}"
|
187
|
+
end
|
188
|
+
|
189
|
+
raise ArgumentError, 'Unable to find Chrome executeable' unless File.file?(@path.to_s) && File.executable?(@path.to_s)
|
190
|
+
|
191
|
+
@path
|
192
|
+
end
|
193
|
+
|
194
|
+
def windows_path
|
195
|
+
raise ArgumentError, 'Not yet Implemented'
|
196
|
+
end
|
197
|
+
|
198
|
+
def macosx_path
|
199
|
+
path = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
200
|
+
path = File.expand_path("~#{path}") unless File.exist?(path)
|
201
|
+
path = find_first_binary('Google Chrome') unless File.exist?(path)
|
202
|
+
path
|
203
|
+
end
|
204
|
+
|
205
|
+
def find_first_binary(*binaries)
|
206
|
+
paths = ENV['PATH'].split(File::PATH_SEPARATOR)
|
207
|
+
|
208
|
+
binaries.each do |binary|
|
209
|
+
paths.each do |path|
|
210
|
+
full_path = File.join(path, binary)
|
211
|
+
exe = Dir.glob(full_path).find { |f| File.executable?(f) }
|
212
|
+
return exe if exe
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|