apparition 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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