apparition 0.0.1

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