percy-selenium 1.1.1 → 1.1.2
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 +4 -4
- data/Gemfile +1 -1
- data/README.md +133 -0
- data/lib/cache.rb +49 -0
- data/lib/driver_metadata.rb +57 -0
- data/lib/percy.rb +261 -39
- data/lib/version.rb +1 -1
- data/package.json +1 -1
- data/spec/lib/percy/cache_spec.rb +127 -0
- data/spec/lib/percy/driver_metadata_spec.rb +139 -0
- data/spec/lib/percy/percy_spec.rb +1025 -37
- data/spec/spec_helper.rb +1 -1
- metadata +9 -6
data/lib/percy.rb
CHANGED
|
@@ -3,15 +3,32 @@ require 'json'
|
|
|
3
3
|
require 'version'
|
|
4
4
|
require 'net/http'
|
|
5
5
|
require 'selenium-webdriver'
|
|
6
|
+
require_relative 'driver_metadata'
|
|
6
7
|
|
|
7
8
|
module Percy
|
|
8
9
|
CLIENT_INFO = "percy-selenium-ruby/#{VERSION}".freeze
|
|
9
10
|
ENV_INFO = "selenium/#{Selenium::WebDriver::VERSION} ruby/#{RUBY_VERSION}".freeze
|
|
10
11
|
|
|
12
|
+
SESSION_TYPE_AUTOMATE = 'automate'.freeze
|
|
13
|
+
SESSION_TYPE_WEB = 'web'.freeze
|
|
14
|
+
|
|
11
15
|
PERCY_DEBUG = ENV['PERCY_LOGLEVEL'] == 'debug'
|
|
12
16
|
PERCY_SERVER_ADDRESS = ENV['PERCY_SERVER_ADDRESS'] || 'http://localhost:5338'
|
|
13
17
|
LABEL = "[\u001b[35m" + (PERCY_DEBUG ? 'percy:ruby' : 'percy') + "\u001b[39m]"
|
|
14
|
-
|
|
18
|
+
RESPONSIVE_CAPTURE_SLEEP_TIME = ENV['RESPONSIVE_CAPTURE_SLEEP_TIME'] ||
|
|
19
|
+
ENV['RESONSIVE_CAPTURE_SLEEP_TIME']
|
|
20
|
+
|
|
21
|
+
def self.responsive_capture_reload_page?
|
|
22
|
+
val = ENV['PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE'] ||
|
|
23
|
+
ENV['PERCY_RESONSIVE_CAPTURE_RELOAD_PAGE'] || 'false'
|
|
24
|
+
val.casecmp('true') == 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.responsive_capture_min_height?
|
|
28
|
+
val = ENV['PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT'] ||
|
|
29
|
+
ENV['PERCY_RESONSIVE_CAPTURE_MIN_HEIGHT'] || 'false'
|
|
30
|
+
val.casecmp('true') == 0
|
|
31
|
+
end
|
|
15
32
|
|
|
16
33
|
def self.create_region(
|
|
17
34
|
bounding_box: nil, element_xpath: nil, element_css: nil, padding: nil,
|
|
@@ -49,16 +66,23 @@ module Percy
|
|
|
49
66
|
region
|
|
50
67
|
end
|
|
51
68
|
|
|
52
|
-
# Take a DOM snapshot and post it to the snapshot endpoint
|
|
53
69
|
def self.snapshot(driver, name, options = {})
|
|
54
70
|
return unless percy_enabled?
|
|
55
71
|
|
|
72
|
+
if @session_type == SESSION_TYPE_AUTOMATE
|
|
73
|
+
raise StandardError, 'Invalid function call - percy_snapshot(). ' \
|
|
74
|
+
'Please use percy_screenshot() function while using Percy with Automate. ' \
|
|
75
|
+
'For more information on usage of percy_screenshot(), ' \
|
|
76
|
+
'refer https://www.browserstack.com/docs/percy/integrate/functional-and-visual'
|
|
77
|
+
end
|
|
78
|
+
|
|
56
79
|
begin
|
|
57
|
-
|
|
80
|
+
percy_dom_script = fetch_percy_dom
|
|
81
|
+
driver.execute_script(percy_dom_script)
|
|
58
82
|
dom_snapshot = if responsive_snapshot_capture?(options)
|
|
59
|
-
capture_responsive_dom(driver, options)
|
|
83
|
+
capture_responsive_dom(driver, options, percy_dom_script: percy_dom_script)
|
|
60
84
|
else
|
|
61
|
-
get_serialized_dom(driver, options)
|
|
85
|
+
get_serialized_dom(driver, options, percy_dom_script: percy_dom_script)
|
|
62
86
|
end
|
|
63
87
|
|
|
64
88
|
response = fetch('percy/snapshot',
|
|
@@ -69,11 +93,11 @@ module Percy
|
|
|
69
93
|
environment_info: ENV_INFO,
|
|
70
94
|
**options,)
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
body = JSON.parse(response.body)
|
|
97
|
+
unless body['success']
|
|
98
|
+
raise StandardError, body['error']
|
|
74
99
|
end
|
|
75
100
|
|
|
76
|
-
body = JSON.parse(response.body)
|
|
77
101
|
body['data']
|
|
78
102
|
rescue StandardError => e
|
|
79
103
|
log("Could not take DOM snapshot '#{name}'")
|
|
@@ -82,7 +106,6 @@ module Percy
|
|
|
82
106
|
end
|
|
83
107
|
|
|
84
108
|
def self.get_browser_instance(driver)
|
|
85
|
-
# this means it is a capybara session
|
|
86
109
|
if driver.respond_to?(:driver) && driver.driver.respond_to?(:browser)
|
|
87
110
|
return driver.driver.browser.manage
|
|
88
111
|
end
|
|
@@ -90,28 +113,131 @@ module Percy
|
|
|
90
113
|
driver.manage
|
|
91
114
|
end
|
|
92
115
|
|
|
93
|
-
def self.get_serialized_dom(driver, options)
|
|
116
|
+
def self.get_serialized_dom(driver, options, percy_dom_script: nil)
|
|
94
117
|
dom_snapshot = driver.execute_script("return PercyDOM.serialize(#{options.to_json})")
|
|
118
|
+
begin
|
|
119
|
+
page_origin = get_origin(driver.current_url)
|
|
120
|
+
iframes = percy_dom_script ? driver.find_elements(:tag_name, 'iframe') : []
|
|
121
|
+
if iframes.any?
|
|
122
|
+
processed_frames = []
|
|
123
|
+
iframes.each do |frame|
|
|
124
|
+
frame_src = frame.attribute('src')
|
|
125
|
+
next if unsupported_iframe_src?(frame_src)
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
frame_origin = get_origin(URI.join(driver.current_url, frame_src).to_s)
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
log("Skipping iframe \"#{frame_src}\": #{e}", 'debug')
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
next if frame_origin == page_origin
|
|
135
|
+
|
|
136
|
+
result = process_frame(driver, frame, options, percy_dom_script)
|
|
137
|
+
processed_frames << result if result
|
|
138
|
+
end
|
|
139
|
+
dom_snapshot['corsIframes'] = processed_frames if processed_frames.any?
|
|
140
|
+
end
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
log("Failed to process cross-origin iframes: #{e}", 'debug')
|
|
143
|
+
begin
|
|
144
|
+
driver.switch_to.default_content
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
95
149
|
|
|
96
150
|
dom_snapshot['cookies'] = get_browser_instance(driver).all_cookies
|
|
97
151
|
dom_snapshot
|
|
98
152
|
end
|
|
99
153
|
|
|
100
|
-
def self.
|
|
101
|
-
|
|
154
|
+
def self.unsupported_iframe_src?(src)
|
|
155
|
+
src.nil? || src.empty? || src == 'about:blank' ||
|
|
156
|
+
src.start_with?('javascript:') || src.start_with?('data:') || src.start_with?('vbscript:')
|
|
157
|
+
end
|
|
102
158
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
159
|
+
def self.get_origin(url)
|
|
160
|
+
uri = URI.parse(url)
|
|
161
|
+
raise URI::InvalidURIError, "no host in #{url}" if uri.host.nil?
|
|
162
|
+
|
|
163
|
+
netloc = uri.host.to_s
|
|
164
|
+
default_ports = {'http' => 80, 'https' => 443}
|
|
165
|
+
netloc += ":#{uri.port}" if uri.port && uri.port != default_ports[uri.scheme]
|
|
166
|
+
"#{uri.scheme}://#{netloc}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.process_frame(driver, frame_element, options, percy_dom_script)
|
|
170
|
+
frame_url = frame_element.attribute('src') || 'unknown-src'
|
|
171
|
+
iframe_snapshot = nil
|
|
172
|
+
|
|
173
|
+
begin
|
|
174
|
+
driver.switch_to.frame(frame_element)
|
|
175
|
+
begin
|
|
176
|
+
driver.execute_script(percy_dom_script)
|
|
177
|
+
iframe_options = options.merge('enableJavaScript' => true)
|
|
178
|
+
iframe_snapshot =
|
|
179
|
+
driver.execute_script("return PercyDOM.serialize(#{iframe_options.to_json})")
|
|
180
|
+
rescue StandardError => e
|
|
181
|
+
log("Failed to process cross-origin frame #{frame_url}: #{e}", 'debug')
|
|
182
|
+
ensure
|
|
183
|
+
begin
|
|
184
|
+
driver.switch_to.default_content
|
|
185
|
+
rescue StandardError
|
|
186
|
+
begin
|
|
187
|
+
driver.switch_to.parent_frame
|
|
188
|
+
rescue StandardError
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
log("Failed to switch to frame #{frame_url}: #{e}", 'debug')
|
|
195
|
+
begin
|
|
196
|
+
driver.switch_to.default_content
|
|
197
|
+
rescue StandardError
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
return nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
return nil if iframe_snapshot.nil?
|
|
204
|
+
|
|
205
|
+
percy_element_id = frame_element.attribute('data-percy-element-id')
|
|
206
|
+
unless percy_element_id
|
|
207
|
+
log("Skipping frame #{frame_url}: no matching percyElementId found", 'debug')
|
|
208
|
+
return nil
|
|
109
209
|
end
|
|
110
210
|
|
|
111
|
-
|
|
211
|
+
{
|
|
212
|
+
'iframeData' => {'percyElementId' => percy_element_id},
|
|
213
|
+
'iframeSnapshot' => iframe_snapshot,
|
|
214
|
+
'frameUrl' => frame_url,
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def self.get_responsive_widths(widths = [])
|
|
219
|
+
begin
|
|
220
|
+
widths_list = widths.is_a?(Array) ? widths : []
|
|
221
|
+
query_param = widths_list.any? ? "?widths=#{widths_list.join(',')}" : ''
|
|
222
|
+
response = fetch("percy/widths-config#{query_param}")
|
|
223
|
+
data = JSON.parse(response.body)
|
|
224
|
+
widths_data = data['widths']
|
|
225
|
+
unless widths_data.is_a?(Array)
|
|
226
|
+
msg = 'Update Percy CLI to the latest version to use responsiveSnapshotCapture'
|
|
227
|
+
raise StandardError, msg
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
widths_data
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
log("Failed to get responsive widths: #{e}.", 'debug')
|
|
233
|
+
raise StandardError, 'Update Percy CLI to the latest version to use ' \
|
|
234
|
+
'responsiveSnapshotCapture'
|
|
235
|
+
end
|
|
112
236
|
end
|
|
113
237
|
|
|
114
238
|
def self.change_window_dimension_and_wait(driver, width, height, resize_count)
|
|
239
|
+
log("Attempting to resize window to #{width}x#{height}", 'debug')
|
|
240
|
+
|
|
115
241
|
begin
|
|
116
242
|
if driver.capabilities.browser_name == 'chrome' && driver.respond_to?(:execute_cdp)
|
|
117
243
|
driver.execute_cdp('Emulation.setDeviceMetricsOverride', {
|
|
@@ -119,50 +245,89 @@ module Percy
|
|
|
119
245
|
},)
|
|
120
246
|
else
|
|
121
247
|
get_browser_instance(driver).window.resize_to(width, height)
|
|
248
|
+
driver.execute_script("window.dispatchEvent(new Event('resize'));")
|
|
122
249
|
end
|
|
123
250
|
rescue StandardError => e
|
|
124
251
|
log("Resizing using cdp failed, falling back to driver for width #{width} #{e}", 'debug')
|
|
125
252
|
get_browser_instance(driver).window.resize_to(width, height)
|
|
253
|
+
driver.execute_script("window.dispatchEvent(new Event('resize'));")
|
|
126
254
|
end
|
|
127
255
|
|
|
128
256
|
begin
|
|
129
257
|
wait = Selenium::WebDriver::Wait.new(timeout: 1)
|
|
130
258
|
wait.until { driver.execute_script('return window.resizeCount') == resize_count }
|
|
259
|
+
actual_size = driver.execute_script('return { w: window.innerWidth, h: window.innerHeight }')
|
|
260
|
+
log("Resize successful. New Viewport Size: #{actual_size['w']}x#{actual_size['h']}", 'debug')
|
|
131
261
|
rescue Selenium::WebDriver::Error::TimeoutError
|
|
132
262
|
log("Timed out waiting for window resize event for width #{width}", 'debug')
|
|
133
263
|
end
|
|
134
264
|
end
|
|
135
265
|
|
|
136
|
-
def self.capture_responsive_dom(driver, options)
|
|
137
|
-
widths =
|
|
266
|
+
def self.capture_responsive_dom(driver, options, percy_dom_script: nil)
|
|
267
|
+
widths = get_responsive_widths(options[:widths] || [])
|
|
138
268
|
dom_snapshots = []
|
|
139
269
|
window_size = get_browser_instance(driver).window.size
|
|
140
270
|
current_width = window_size.width
|
|
141
271
|
current_height = window_size.height
|
|
142
272
|
last_window_width = current_width
|
|
273
|
+
last_window_height = current_height
|
|
143
274
|
resize_count = 0
|
|
144
275
|
driver.execute_script('PercyDOM.waitForResize()')
|
|
276
|
+
target_height = current_height
|
|
145
277
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
278
|
+
if responsive_capture_min_height?
|
|
279
|
+
min = options[:minHeight] || @cli_config&.dig('snapshot', 'minHeight')
|
|
280
|
+
if min
|
|
281
|
+
target_height = min
|
|
282
|
+
else
|
|
283
|
+
log('PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT is enabled but no minHeight value ' \
|
|
284
|
+
'was provided in options or CLI config; using current window height', 'debug',)
|
|
151
285
|
end
|
|
286
|
+
end
|
|
152
287
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
288
|
+
begin
|
|
289
|
+
widths.each do |width_dict|
|
|
290
|
+
width = width_dict['width']
|
|
291
|
+
height = width_dict['height'] || target_height
|
|
292
|
+
|
|
293
|
+
if last_window_width != width || last_window_height != height
|
|
294
|
+
resize_count += 1
|
|
295
|
+
change_window_dimension_and_wait(driver, width, height, resize_count)
|
|
296
|
+
last_window_width = width
|
|
297
|
+
last_window_height = height
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
if responsive_capture_reload_page?
|
|
301
|
+
log("Reloading page for width: #{width}", 'debug')
|
|
302
|
+
begin
|
|
303
|
+
driver.navigate.refresh
|
|
304
|
+
rescue StandardError
|
|
305
|
+
begin
|
|
306
|
+
driver.driver.browser.navigate.refresh
|
|
307
|
+
rescue StandardError => e
|
|
308
|
+
log("Failed to refresh page: #{e}", 'debug')
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
percy_dom_script = fetch_percy_dom
|
|
312
|
+
driver.execute_script(percy_dom_script)
|
|
313
|
+
driver.execute_script('PercyDOM.waitForResize()')
|
|
314
|
+
resize_count = 0
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
sleep(RESPONSIVE_CAPTURE_SLEEP_TIME.to_i) if RESPONSIVE_CAPTURE_SLEEP_TIME
|
|
318
|
+
|
|
319
|
+
dom_snapshot = get_serialized_dom(driver, options, percy_dom_script: percy_dom_script)
|
|
320
|
+
dom_snapshot['width'] = width
|
|
321
|
+
dom_snapshots << dom_snapshot
|
|
322
|
+
end
|
|
323
|
+
ensure
|
|
324
|
+
change_window_dimension_and_wait(driver, current_width, current_height, resize_count + 1)
|
|
158
325
|
end
|
|
159
326
|
|
|
160
|
-
change_window_dimension_and_wait(driver, current_width, current_height, resize_count + 1)
|
|
161
327
|
dom_snapshots
|
|
162
328
|
end
|
|
163
329
|
|
|
164
330
|
def self.responsive_snapshot_capture?(options)
|
|
165
|
-
# Don't run responsive snapshot capture when defer uploads is enabled
|
|
166
331
|
return false if @cli_config&.dig('percy', 'deferUploads')
|
|
167
332
|
|
|
168
333
|
options[:responsive_snapshot_capture] ||
|
|
@@ -170,7 +335,6 @@ module Percy
|
|
|
170
335
|
@cli_config&.dig('snapshot', 'responsiveSnapshotCapture')
|
|
171
336
|
end
|
|
172
337
|
|
|
173
|
-
# Determine if the Percy server is running, caching the result so it is only checked once
|
|
174
338
|
def self.percy_enabled?
|
|
175
339
|
return @percy_enabled unless @percy_enabled.nil?
|
|
176
340
|
|
|
@@ -194,8 +358,8 @@ module Percy
|
|
|
194
358
|
end
|
|
195
359
|
|
|
196
360
|
response_body = JSON.parse(response.body)
|
|
197
|
-
@eligible_widths = response_body['widths']
|
|
198
361
|
@cli_config = response_body['config']
|
|
362
|
+
@session_type = response_body['type']
|
|
199
363
|
@percy_enabled = true
|
|
200
364
|
true
|
|
201
365
|
rescue StandardError => e
|
|
@@ -206,7 +370,6 @@ module Percy
|
|
|
206
370
|
end
|
|
207
371
|
end
|
|
208
372
|
|
|
209
|
-
# Fetch the @percy/dom script, caching the result so it is only fetched once
|
|
210
373
|
def self.fetch_percy_dom
|
|
211
374
|
return @percy_dom unless @percy_dom.nil?
|
|
212
375
|
|
|
@@ -229,8 +392,6 @@ module Percy
|
|
|
229
392
|
end
|
|
230
393
|
end
|
|
231
394
|
|
|
232
|
-
# Make an HTTP request (GET,POST) using Ruby's Net::HTTP. If `data` is present,
|
|
233
|
-
# `fetch` will POST as JSON.
|
|
234
395
|
def self.fetch(url, data = nil)
|
|
235
396
|
uri = URI("#{PERCY_SERVER_ADDRESS}/#{url}")
|
|
236
397
|
|
|
@@ -251,10 +412,71 @@ module Percy
|
|
|
251
412
|
response
|
|
252
413
|
end
|
|
253
414
|
|
|
415
|
+
def self.percy_screenshot(driver, name, options = {})
|
|
416
|
+
return unless percy_enabled?
|
|
417
|
+
|
|
418
|
+
unless @session_type == SESSION_TYPE_AUTOMATE
|
|
419
|
+
raise StandardError, 'Invalid function call - percy_screenshot(). ' \
|
|
420
|
+
'Please use percy_snapshot() function for taking screenshot. ' \
|
|
421
|
+
'percy_screenshot() should be used only while using Percy with Automate. ' \
|
|
422
|
+
'For more information on usage of percy_snapshot(), ' \
|
|
423
|
+
'refer doc for your language https://www.browserstack.com/docs/percy/integrate/overview'
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
begin
|
|
427
|
+
options = options.dup
|
|
428
|
+
metadata = get_driver_metadata(driver)
|
|
429
|
+
|
|
430
|
+
if options.key?(:ignoreRegionSeleniumElements)
|
|
431
|
+
options[:ignore_region_selenium_elements] = options.delete(:ignoreRegionSeleniumElements)
|
|
432
|
+
end
|
|
433
|
+
if options.key?(:considerRegionSeleniumElements)
|
|
434
|
+
options[:consider_region_selenium_elements] =
|
|
435
|
+
options.delete(:considerRegionSeleniumElements)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
ignore_region_elements =
|
|
439
|
+
get_element_ids(options.delete(:ignore_region_selenium_elements) || [])
|
|
440
|
+
consider_region_elements =
|
|
441
|
+
get_element_ids(options.delete(:consider_region_selenium_elements) || [])
|
|
442
|
+
|
|
443
|
+
options[:ignore_region_elements] = ignore_region_elements
|
|
444
|
+
options[:consider_region_elements] = consider_region_elements
|
|
445
|
+
|
|
446
|
+
response = fetch('percy/automateScreenshot',
|
|
447
|
+
client_info: CLIENT_INFO,
|
|
448
|
+
environment_info: ENV_INFO,
|
|
449
|
+
sessionId: metadata.session_id,
|
|
450
|
+
commandExecutorUrl: metadata.command_executor_url,
|
|
451
|
+
capabilities: metadata.capabilities,
|
|
452
|
+
snapshotName: name,
|
|
453
|
+
options: options,)
|
|
454
|
+
|
|
455
|
+
body = JSON.parse(response.body)
|
|
456
|
+
unless body['success']
|
|
457
|
+
raise StandardError, body['error']
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
body['data']
|
|
461
|
+
rescue StandardError => e
|
|
462
|
+
log("Could not take Screenshot '#{name}'")
|
|
463
|
+
log(e, 'debug')
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def self.get_driver_metadata(driver)
|
|
468
|
+
DriverMetaData.new(driver)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def self.get_element_ids(elements)
|
|
472
|
+
elements.map(&:id)
|
|
473
|
+
end
|
|
474
|
+
|
|
254
475
|
def self._clear_cache!
|
|
255
476
|
@percy_dom = nil
|
|
256
477
|
@percy_enabled = nil
|
|
257
|
-
@eligible_widths = nil
|
|
258
478
|
@cli_config = nil
|
|
479
|
+
@session_type = nil
|
|
480
|
+
Cache.clear_cache!
|
|
259
481
|
end
|
|
260
482
|
end
|
data/lib/version.rb
CHANGED
data/package.json
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Cache do
|
|
4
|
+
let(:session_id) { 'session_id_123' }
|
|
5
|
+
let(:url) { 'https://example-hub:4444/wd/hub' }
|
|
6
|
+
let(:caps) { {'browser' => 'chrome', 'platform' => 'windows', 'browserVersion' => '115.0.1'} }
|
|
7
|
+
|
|
8
|
+
before(:each) { Cache.clear_cache! }
|
|
9
|
+
|
|
10
|
+
describe '.check_types' do
|
|
11
|
+
it 'raises TypeError when session_id is not a string' do
|
|
12
|
+
expect { Cache.check_types(123, Cache::COMMAND_EXECUTOR_URL) }
|
|
13
|
+
.to raise_error(TypeError, 'Argument session_id should be string')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'raises TypeError when property is not a string' do
|
|
17
|
+
expect { Cache.check_types(session_id, 123) }
|
|
18
|
+
.to raise_error(TypeError, 'Argument property should be string')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'does not raise when both arguments are strings' do
|
|
22
|
+
expect { Cache.check_types(session_id, Cache::COMMAND_EXECUTOR_URL) }.to_not raise_error
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '.set_cache' do
|
|
27
|
+
it 'raises TypeError when session_id is not a string' do
|
|
28
|
+
expect { Cache.set_cache(123, Cache::COMMAND_EXECUTOR_URL, url) }
|
|
29
|
+
.to raise_error(TypeError, 'Argument session_id should be string')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'raises TypeError when property is not a string' do
|
|
33
|
+
expect { Cache.set_cache(session_id, 123, url) }
|
|
34
|
+
.to raise_error(TypeError, 'Argument property should be string')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'stores the command_executor_url in cache with a timestamp' do
|
|
38
|
+
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
|
|
39
|
+
expect(Cache::CACHE[session_id][Cache::COMMAND_EXECUTOR_URL]).to eq(url)
|
|
40
|
+
expect(Cache::CACHE[session_id][Cache::TIMEOUT_KEY]).to be_a(Float)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'stores the capabilities in cache' do
|
|
44
|
+
Cache.set_cache(session_id, Cache::CAPABILITIES, caps)
|
|
45
|
+
expect(Cache::CACHE[session_id][Cache::CAPABILITIES]).to eq(caps)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'updates an existing session entry' do
|
|
49
|
+
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
|
|
50
|
+
new_url = 'https://new-hub:4444/wd/hub'
|
|
51
|
+
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, new_url)
|
|
52
|
+
expect(Cache::CACHE[session_id][Cache::COMMAND_EXECUTOR_URL]).to eq(new_url)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '.get_cache' do
|
|
57
|
+
before(:each) do
|
|
58
|
+
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
|
|
59
|
+
Cache.set_cache(session_id, Cache::CAPABILITIES, caps)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'raises TypeError when session_id is not a string' do
|
|
63
|
+
expect { Cache.get_cache(123, Cache::COMMAND_EXECUTOR_URL) }
|
|
64
|
+
.to raise_error(TypeError, 'Argument session_id should be string')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'raises TypeError when property is not a string' do
|
|
68
|
+
expect { Cache.get_cache(session_id, 123) }
|
|
69
|
+
.to raise_error(TypeError, 'Argument property should be string')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'returns the cached command_executor_url' do
|
|
73
|
+
expect(Cache.get_cache(session_id, Cache::COMMAND_EXECUTOR_URL)).to eq(url)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'returns the cached capabilities' do
|
|
77
|
+
expect(Cache.get_cache(session_id, Cache::CAPABILITIES)).to eq(caps)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'returns nil for a missing property' do
|
|
81
|
+
expect(Cache.get_cache(session_id, 'nonexistent_key')).to be_nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns nil for an unknown session' do
|
|
85
|
+
expect(Cache.get_cache('unknown_session', Cache::COMMAND_EXECUTOR_URL)).to be_nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'calls cleanup_cache' do
|
|
89
|
+
expect(Cache).to receive(:cleanup_cache).and_call_original # private method expectation
|
|
90
|
+
Cache.get_cache(session_id, Cache::COMMAND_EXECUTOR_URL)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe '.cleanup_cache (private, tested via send)' do
|
|
95
|
+
it 'removes entries that have exceeded the cache timeout' do
|
|
96
|
+
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
|
|
97
|
+
Cache::CACHE[session_id][Cache::TIMEOUT_KEY] = Time.now.to_f - (Cache::CACHE_TIMEOUT + 1)
|
|
98
|
+
Cache.send(:cleanup_cache)
|
|
99
|
+
expect(Cache::CACHE).to_not have_key(session_id)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'keeps entries that have not exceeded the cache timeout' do
|
|
103
|
+
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
|
|
104
|
+
Cache.send(:cleanup_cache)
|
|
105
|
+
expect(Cache::CACHE).to have_key(session_id)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'only removes expired entries, leaving valid ones' do
|
|
109
|
+
expired_session = 'expired_session'
|
|
110
|
+
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
|
|
111
|
+
Cache.set_cache(expired_session, Cache::COMMAND_EXECUTOR_URL, url)
|
|
112
|
+
Cache::CACHE[expired_session][Cache::TIMEOUT_KEY] = Time.now.to_f - (Cache::CACHE_TIMEOUT + 1)
|
|
113
|
+
Cache.send(:cleanup_cache)
|
|
114
|
+
expect(Cache::CACHE).to have_key(session_id)
|
|
115
|
+
expect(Cache::CACHE).to_not have_key(expired_session)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe '.clear_cache!' do
|
|
120
|
+
it 'removes all entries from the cache' do
|
|
121
|
+
Cache.set_cache(session_id, Cache::COMMAND_EXECUTOR_URL, url)
|
|
122
|
+
Cache.set_cache('other_session', Cache::CAPABILITIES, caps)
|
|
123
|
+
Cache.clear_cache!
|
|
124
|
+
expect(Cache::CACHE).to be_empty
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|