apparition 0.0.1

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.
@@ -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