capybara-lightpanda 0.1.0

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,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Lightpanda
5
+ class Keyboard # rubocop:disable Metrics/ClassLength
6
+ # Capybara symbol -> CDP key definition.
7
+ KEYS = {
8
+ cancel: { key: "Cancel", code: "Abort", keyCode: 3 },
9
+ help: { key: "Help", code: "Help", keyCode: 6 },
10
+ backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
11
+ tab: { key: "Tab", code: "Tab", keyCode: 9 },
12
+ clear: { key: "Clear", code: "NumLock", keyCode: 12 },
13
+ return: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
14
+ enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
15
+ shift: { key: "Shift", code: "ShiftLeft", keyCode: 16 },
16
+ control: { key: "Control", code: "ControlLeft", keyCode: 17 },
17
+ alt: { key: "Alt", code: "AltLeft", keyCode: 18 },
18
+ pause: { key: "Pause", code: "Pause", keyCode: 19 },
19
+ escape: { key: "Escape", code: "Escape", keyCode: 27 },
20
+ space: { key: " ", code: "Space", keyCode: 32, text: " " },
21
+ page_up: { key: "PageUp", code: "PageUp", keyCode: 33 },
22
+ page_down: { key: "PageDown", code: "PageDown", keyCode: 34 },
23
+ end: { key: "End", code: "End", keyCode: 35 },
24
+ home: { key: "Home", code: "Home", keyCode: 36 },
25
+ left: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
26
+ up: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
27
+ right: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
28
+ down: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
29
+ insert: { key: "Insert", code: "Insert", keyCode: 45 },
30
+ delete: { key: "Delete", code: "Delete", keyCode: 46 },
31
+ semicolon: { key: ";", code: "Semicolon", keyCode: 186, text: ";" },
32
+ equals: { key: "=", code: "Equal", keyCode: 187, text: "=" },
33
+ numpad0: { key: "0", code: "Numpad0", keyCode: 96, text: "0" },
34
+ numpad1: { key: "1", code: "Numpad1", keyCode: 97, text: "1" },
35
+ numpad2: { key: "2", code: "Numpad2", keyCode: 98, text: "2" },
36
+ numpad3: { key: "3", code: "Numpad3", keyCode: 99, text: "3" },
37
+ numpad4: { key: "4", code: "Numpad4", keyCode: 100, text: "4" },
38
+ numpad5: { key: "5", code: "Numpad5", keyCode: 101, text: "5" },
39
+ numpad6: { key: "6", code: "Numpad6", keyCode: 102, text: "6" },
40
+ numpad7: { key: "7", code: "Numpad7", keyCode: 103, text: "7" },
41
+ numpad8: { key: "8", code: "Numpad8", keyCode: 104, text: "8" },
42
+ numpad9: { key: "9", code: "Numpad9", keyCode: 105, text: "9" },
43
+ multiply: { key: "*", code: "NumpadMultiply", keyCode: 106, text: "*" },
44
+ add: { key: "+", code: "NumpadAdd", keyCode: 107, text: "+" },
45
+ separator: { key: ".", code: "NumpadDecimal", keyCode: 110, text: "." },
46
+ subtract: { key: "-", code: "NumpadSubtract", keyCode: 109, text: "-" },
47
+ decimal: { key: ".", code: "NumpadDecimal", keyCode: 110, text: "." },
48
+ divide: { key: "/", code: "NumpadDivide", keyCode: 111, text: "/" },
49
+ f1: { key: "F1", code: "F1", keyCode: 112 },
50
+ f2: { key: "F2", code: "F2", keyCode: 113 },
51
+ f3: { key: "F3", code: "F3", keyCode: 114 },
52
+ f4: { key: "F4", code: "F4", keyCode: 115 },
53
+ f5: { key: "F5", code: "F5", keyCode: 116 },
54
+ f6: { key: "F6", code: "F6", keyCode: 117 },
55
+ f7: { key: "F7", code: "F7", keyCode: 118 },
56
+ f8: { key: "F8", code: "F8", keyCode: 119 },
57
+ f9: { key: "F9", code: "F9", keyCode: 120 },
58
+ f10: { key: "F10", code: "F10", keyCode: 121 },
59
+ f11: { key: "F11", code: "F11", keyCode: 122 },
60
+ f12: { key: "F12", code: "F12", keyCode: 123 },
61
+ meta: { key: "Meta", code: "MetaLeft", keyCode: 91 },
62
+ command: { key: "Meta", code: "MetaLeft", keyCode: 91 },
63
+ }.freeze
64
+
65
+ MODIFIERS = {
66
+ alt: 1,
67
+ control: 2, ctrl: 2,
68
+ meta: 4, command: 4,
69
+ shift: 8,
70
+ }.freeze
71
+
72
+ def initialize(browser)
73
+ @browser = browser
74
+ end
75
+
76
+ def type(*keys)
77
+ keys.each do |key|
78
+ case key
79
+ when Symbol then dispatch_key(key)
80
+ when String then key.each_char { |char| dispatch_char(char) }
81
+ when Array then type_with_modifiers(key)
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def dispatch_key(key)
89
+ definition = KEYS.fetch(key) { raise ArgumentError, "Unknown key: #{key.inspect}" }
90
+ raw_dispatch(definition)
91
+ end
92
+
93
+ def dispatch_char(char)
94
+ @browser.page_command("Input.insertText", text: char)
95
+ end
96
+
97
+ def type_with_modifiers(keys)
98
+ modifiers, chars = keys.partition { |k| k.is_a?(Symbol) && MODIFIERS.key?(k) }
99
+ modifier_value = modifiers.sum { |m| MODIFIERS[m] }
100
+
101
+ modifiers.each { |m| send_key_event("rawKeyDown", KEYS[m]) }
102
+ chars.each { |key| dispatch_modified(key, modifier_value, modifiers) }
103
+ modifiers.reverse_each { |m| send_key_event("keyUp", KEYS[m]) }
104
+ end
105
+
106
+ def dispatch_modified(key, modifier_value, modifiers)
107
+ case key
108
+ when Symbol
109
+ definition = KEYS.fetch(key) { raise ArgumentError, "Unknown key: #{key.inspect}" }
110
+ raw_dispatch(definition, modifiers: modifier_value)
111
+ when String
112
+ key.each_char { |char| dispatch_modified_char(char, modifier_value, modifiers) }
113
+ end
114
+ end
115
+
116
+ def dispatch_modified_char(char, modifier_value, modifiers)
117
+ text = modifiers.include?(:shift) ? char.upcase : char
118
+ send_key_event("keyDown", { key: text, text: text, unmodifiedText: char }, modifiers: modifier_value)
119
+ send_key_event("keyUp", { key: text }, modifiers: modifier_value)
120
+ end
121
+
122
+ def raw_dispatch(definition, modifiers: 0)
123
+ type = definition[:text] ? "keyDown" : "rawKeyDown"
124
+ send_key_event(type, definition, modifiers: modifiers)
125
+ send_key_event("keyUp", definition, modifiers: modifiers)
126
+ end
127
+
128
+ def send_key_event(type, definition, modifiers: 0)
129
+ params = {
130
+ type: type,
131
+ key: definition[:key],
132
+ code: definition[:code],
133
+ }
134
+ params[:windowsVirtualKeyCode] = definition[:keyCode] if definition[:keyCode]
135
+ params[:text] = definition[:text] if definition[:text]
136
+ params[:unmodifiedText] = definition[:unmodifiedText] || definition[:text] if definition[:text]
137
+ params[:modifiers] = modifiers if modifiers.positive?
138
+ @browser.page_command("Input.dispatchKeyEvent", **params)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Lightpanda
5
+ class Logger
6
+ attr_reader :output
7
+
8
+ def initialize(output = nil)
9
+ @output = output
10
+ @suppressed = false
11
+ @started_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
12
+ end
13
+
14
+ def puts(message)
15
+ return if @suppressed || @output.nil?
16
+
17
+ @output.puts(message)
18
+ end
19
+
20
+ def suppress
21
+ prev = @suppressed
22
+ @suppressed = true
23
+ yield
24
+ ensure
25
+ @suppressed = prev
26
+ end
27
+
28
+ def suppressed?
29
+ @suppressed
30
+ end
31
+
32
+ def elapsed_time
33
+ format("%.3fs", ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @started_at)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Lightpanda
5
+ class Network
6
+ attr_reader :browser
7
+
8
+ def initialize(browser)
9
+ @browser = browser
10
+ @traffic = []
11
+ @enabled = false
12
+ end
13
+
14
+ def enable
15
+ return if @enabled
16
+
17
+ browser.command("Network.enable")
18
+ subscribe
19
+ @enabled = true
20
+ end
21
+
22
+ def disable
23
+ return unless @enabled
24
+
25
+ browser.command("Network.disable")
26
+ @enabled = false
27
+ end
28
+
29
+ def traffic
30
+ @traffic.dup
31
+ end
32
+
33
+ def clear
34
+ @traffic.clear
35
+ end
36
+
37
+ def headers=(headers)
38
+ @extra_headers = headers
39
+ browser.page_command("Network.setExtraHTTPHeaders", headers: headers)
40
+ end
41
+
42
+ def add_headers(headers)
43
+ @extra_headers = (@extra_headers || {}).merge(headers)
44
+ browser.page_command("Network.setExtraHTTPHeaders", headers: @extra_headers)
45
+ end
46
+
47
+ def clear_headers
48
+ @extra_headers = {}
49
+ browser.page_command("Network.setExtraHTTPHeaders", headers: {})
50
+ end
51
+
52
+ def wait_for_idle(timeout: 5, connections: 0) # rubocop:disable Naming/PredicateMethod
53
+ started_at = Time.now
54
+
55
+ while Time.now - started_at < timeout
56
+ pending = @traffic.count { |t| t[:response].nil? }
57
+ return true if pending <= connections
58
+
59
+ sleep 0.1
60
+ end
61
+
62
+ false
63
+ end
64
+
65
+ private
66
+
67
+ def subscribe
68
+ browser.on("Network.requestWillBeSent") do |params|
69
+ @traffic << {
70
+ request_id: params["requestId"],
71
+ url: params.dig("request", "url"),
72
+ method: params.dig("request", "method"),
73
+ timestamp: params["timestamp"],
74
+ response: nil,
75
+ }
76
+ end
77
+
78
+ browser.on("Network.responseReceived") do |params|
79
+ request = @traffic.find { |t| t[:request_id] == params["requestId"] }
80
+
81
+ next unless request
82
+
83
+ request[:response] = {
84
+ status: params.dig("response", "status"),
85
+ headers: params.dig("response", "headers"),
86
+ mime_type: params.dig("response", "mimeType"),
87
+ }
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end