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.
- 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,230 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara
|
4
|
+
module Apparition
|
5
|
+
class Error < StandardError; end
|
6
|
+
class NoSuchWindowError < Error; end
|
7
|
+
|
8
|
+
class ClientError < Error
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class CDPError < Error
|
17
|
+
attr_reader :message, :code
|
18
|
+
|
19
|
+
def initialize(error)
|
20
|
+
@message = error['message']
|
21
|
+
@code = error['code']
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class JSErrorItem
|
26
|
+
attr_reader :message, :stack
|
27
|
+
|
28
|
+
def initialize(message, stack)
|
29
|
+
@message = message
|
30
|
+
@stack = stack
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
[message, stack].join("\n")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class BrowserError < ClientError
|
39
|
+
def name
|
40
|
+
response['name']
|
41
|
+
end
|
42
|
+
|
43
|
+
def error_parameters
|
44
|
+
(response['args'] || []).join("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
def message
|
48
|
+
'There was an error inside the Puppeteer portion of Apparition. ' \
|
49
|
+
'If this is the error returned, and not the cause of a more detailed error response, ' \
|
50
|
+
'this is probably a bug, so please report it. ' \
|
51
|
+
"\n\n#{name}: #{error_parameters}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class JavascriptError < ClientError
|
56
|
+
# def javascript_errors
|
57
|
+
# response['args'].first.map { |data| JSErrorItem.new(data['message'], data['stack']) }
|
58
|
+
# end
|
59
|
+
def javascript_errors
|
60
|
+
[response]
|
61
|
+
end
|
62
|
+
|
63
|
+
def message
|
64
|
+
'One or more errors were raised in the Javascript code on the page. ' \
|
65
|
+
"If you don't care about these errors, you can ignore them by " \
|
66
|
+
'setting js_errors: false in your Apparition configuration (see ' \
|
67
|
+
'documentation for details).' \
|
68
|
+
"\n\n#{javascript_errors.map(&:to_s).join("\n")}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class StatusFailError < ClientError
|
73
|
+
def url
|
74
|
+
response['args'].first
|
75
|
+
end
|
76
|
+
|
77
|
+
def details
|
78
|
+
response['args'][1]
|
79
|
+
end
|
80
|
+
|
81
|
+
def message
|
82
|
+
msg = "Request to '#{url}' failed to reach server, check DNS and/or server status"
|
83
|
+
msg += " - #{details}" if details
|
84
|
+
msg
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class FrameNotFound < ClientError
|
89
|
+
def name
|
90
|
+
response['args'].first
|
91
|
+
end
|
92
|
+
|
93
|
+
def message
|
94
|
+
"The frame '#{name}' was not found."
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class InvalidSelector < ClientError
|
99
|
+
def method
|
100
|
+
response['args'][0]
|
101
|
+
end
|
102
|
+
|
103
|
+
def selector
|
104
|
+
response['args'][1]
|
105
|
+
end
|
106
|
+
|
107
|
+
def message
|
108
|
+
'The browser raised a syntax error while trying to evaluate ' \
|
109
|
+
"#{method} selector #{selector.inspect}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class NodeError < ClientError
|
114
|
+
attr_reader :node
|
115
|
+
|
116
|
+
def initialize(node, response)
|
117
|
+
@node = node
|
118
|
+
super(response)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class ObsoleteNode < NodeError
|
123
|
+
def message
|
124
|
+
'The element you are trying to interact with is either not part of the DOM, or is ' \
|
125
|
+
'not currently visible on the page (perhaps display: none is set). ' \
|
126
|
+
"It's possible the element has been replaced by another element and you meant to interact with " \
|
127
|
+
"the new element. If so you need to do a new 'find' in order to get a reference to the " \
|
128
|
+
'new element.'
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class WrongWorld < ObsoleteNode
|
133
|
+
def message
|
134
|
+
'The element you are trying to access is not from the current page'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class UnsupportedFeature < ClientError
|
139
|
+
def name
|
140
|
+
response['name']
|
141
|
+
end
|
142
|
+
|
143
|
+
def unsupported_message
|
144
|
+
response['args'][0]
|
145
|
+
end
|
146
|
+
|
147
|
+
def version
|
148
|
+
response['args'][1].values_at('major', 'minor', 'patch').join '.'
|
149
|
+
end
|
150
|
+
|
151
|
+
def message
|
152
|
+
"Running version of Chrome #{version} does not support some feature: #{unsupported_message}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class MouseEventFailed < NodeError
|
157
|
+
def name
|
158
|
+
response['args'][0]
|
159
|
+
end
|
160
|
+
|
161
|
+
def selector
|
162
|
+
response['args'][1]
|
163
|
+
end
|
164
|
+
|
165
|
+
def position
|
166
|
+
[response['args'][2][:x], response['args'][2][:y]]
|
167
|
+
end
|
168
|
+
|
169
|
+
def message
|
170
|
+
"Firing a #{name} at co-ordinates [#{position.join(', ')}] failed. Apparition detected " \
|
171
|
+
"another element with CSS selector '#{selector}' at this position. " \
|
172
|
+
'It may be overlapping the element you are trying to interact with. '
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class MouseEventImpossible < MouseEventFailed
|
177
|
+
def name
|
178
|
+
response['args'][0]
|
179
|
+
end
|
180
|
+
|
181
|
+
def selector
|
182
|
+
nil
|
183
|
+
end
|
184
|
+
|
185
|
+
def position
|
186
|
+
nil
|
187
|
+
end
|
188
|
+
|
189
|
+
def message
|
190
|
+
"Firing a #{name} event is not possible since the element has no visible position on the page."
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
class KeyError < ::ArgumentError
|
195
|
+
def initialize(key)
|
196
|
+
super(key)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class TimeoutError < Error
|
201
|
+
def initialize(message = nil)
|
202
|
+
@message = message
|
203
|
+
end
|
204
|
+
|
205
|
+
def message
|
206
|
+
"Timed out waiting for response to #{@message}. It's possible that this happened " \
|
207
|
+
'because something took a very long time (for example a page load was slow). ' \
|
208
|
+
'If so, setting the Apparition :timeout option to a higher value will help ' \
|
209
|
+
'(see the docs for details). If increasing the timeout does not help, this is ' \
|
210
|
+
'probably a bug in Apparition - please report it to the issue tracker.'
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class ScriptTimeoutError < Error
|
215
|
+
def message
|
216
|
+
'Timed out waiting for evaluated script to resturn a value'
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class DeadClient < Error
|
221
|
+
def initialize(message)
|
222
|
+
@message = message
|
223
|
+
end
|
224
|
+
|
225
|
+
def message
|
226
|
+
"Chrome client died while processing #{@message}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Apparition
|
4
|
+
class Frame
|
5
|
+
attr_reader :id, :parent_id
|
6
|
+
attr_accessor :element_id
|
7
|
+
|
8
|
+
def initialize(page, params)
|
9
|
+
@page = page
|
10
|
+
@id = params[:frameId] || params['frameId'] || params['id']
|
11
|
+
@parent_id = params['parentFrameId'] || params['parentId']
|
12
|
+
@context_id = nil
|
13
|
+
@state = nil
|
14
|
+
@element_id = nil
|
15
|
+
@frame_mutex = Mutex.new
|
16
|
+
@loader_id = @prev_loader_id = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def context_id
|
20
|
+
@frame_mutex.synchronize do
|
21
|
+
@context_id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def context_id=(id)
|
26
|
+
@frame_mutex.synchronize do
|
27
|
+
@context_id = id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def state=(state)
|
32
|
+
@frame_mutex.synchronize do
|
33
|
+
@state = state
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def state
|
38
|
+
@frame_mutex.synchronize do
|
39
|
+
@state
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def loader_id
|
44
|
+
@frame_mutex.synchronize do
|
45
|
+
@loader_id
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def loading(id)
|
50
|
+
self.loader_id = id
|
51
|
+
end
|
52
|
+
|
53
|
+
def reloading!
|
54
|
+
self.loader_id = @prev_loader_id
|
55
|
+
end
|
56
|
+
|
57
|
+
def loading?
|
58
|
+
!@loader_id.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
def loaded?
|
62
|
+
@loader_id.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
def loaded!
|
66
|
+
@prev_loader_id = loader_id
|
67
|
+
self.loader_id = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def obsolete!
|
71
|
+
self.state = :obsolete
|
72
|
+
end
|
73
|
+
|
74
|
+
def obsolete?
|
75
|
+
state == :obsolete
|
76
|
+
end
|
77
|
+
|
78
|
+
def usable?
|
79
|
+
context_id && !loading?
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def loader_id=(id)
|
85
|
+
@frame_mutex.synchronize do
|
86
|
+
@loader_id = id
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/apparition/frame'
|
4
|
+
|
5
|
+
module Capybara::Apparition
|
6
|
+
class FrameManager
|
7
|
+
def initialize(id)
|
8
|
+
@frames = {}
|
9
|
+
@frames_mutex = Mutex.new
|
10
|
+
add(id)
|
11
|
+
@main_id = @current_id = id
|
12
|
+
end
|
13
|
+
|
14
|
+
def main
|
15
|
+
get(@main_id)
|
16
|
+
end
|
17
|
+
|
18
|
+
def current
|
19
|
+
get(@current_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def pop_frame(top:)
|
23
|
+
@current_id = if top
|
24
|
+
@main_id
|
25
|
+
else
|
26
|
+
get(@current_id).parent_id
|
27
|
+
end
|
28
|
+
cleanup_unused_obsolete
|
29
|
+
end
|
30
|
+
|
31
|
+
def push_frame(id)
|
32
|
+
@current_id = id
|
33
|
+
end
|
34
|
+
|
35
|
+
def add(id, frame_params = {})
|
36
|
+
@frames_mutex.synchronize do
|
37
|
+
@frames[id] = Frame.new(nil, frame_params.merge(frameId: id))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def get(id)
|
42
|
+
@frames_mutex.synchronize do
|
43
|
+
@frames[id]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(id)
|
48
|
+
@frames_mutex.synchronize do
|
49
|
+
if @current_id == id
|
50
|
+
@frames[id].obsolete!
|
51
|
+
else
|
52
|
+
@frames.delete(id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def exists?(id)
|
58
|
+
@frames_mutex.synchronize do
|
59
|
+
@frames.key?(id)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def destroy_context(ctx_id)
|
64
|
+
@frames_mutex.synchronize do
|
65
|
+
@frames.each_value do |f|
|
66
|
+
f.context_id = nil if f.context_id == ctx_id
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def cleanup_unused_obsolete
|
74
|
+
@frames_mutex.synchronize do
|
75
|
+
@frames.delete_if do |_id, f|
|
76
|
+
f.obsolete? && (f.id != @current_id)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Apparition
|
4
|
+
class Inspector
|
5
|
+
# TODO: IS this necessary anymore?
|
6
|
+
BROWSERS = %w[chromium chromium-browser google-chrome open].freeze
|
7
|
+
DEFAULT_PORT = 9664
|
8
|
+
|
9
|
+
def self.detect_browser
|
10
|
+
@browser ||= BROWSERS.find { |name| browser_binary_exists?(name) }
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :port
|
14
|
+
|
15
|
+
def initialize(browser = nil, port = DEFAULT_PORT)
|
16
|
+
@browser = browser.respond_to?(:to_str) ? browser : nil
|
17
|
+
@port = port
|
18
|
+
end
|
19
|
+
|
20
|
+
def browser
|
21
|
+
@browser ||= self.class.detect_browser
|
22
|
+
end
|
23
|
+
|
24
|
+
def url(scheme)
|
25
|
+
"#{scheme}://localhost:#{port}/"
|
26
|
+
end
|
27
|
+
|
28
|
+
def open(scheme)
|
29
|
+
if browser
|
30
|
+
Process.spawn(browser, url(scheme))
|
31
|
+
else
|
32
|
+
raise Error, "Could not find a browser executable to open #{url(scheme)}. " \
|
33
|
+
"You can specify one manually using e.g. `:inspector => 'chromium'` " \
|
34
|
+
'as a configuration option for Apparition.'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.browser_binary_exists?(browser)
|
39
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
40
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
41
|
+
exts.each do |ext|
|
42
|
+
exe = "#{path}#{File::SEPARATOR}#{browser}#{ext}"
|
43
|
+
return exe if File.executable? exe
|
44
|
+
end
|
45
|
+
end
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|