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,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