automation_helpers 4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +13 -0
- data/README.md +49 -0
- data/lib/automation_helpers/drivers/browserstack.rb +9 -0
- data/lib/automation_helpers/drivers/local.rb +9 -0
- data/lib/automation_helpers/drivers/remote.rb +9 -0
- data/lib/automation_helpers/drivers/v4/browserstack.rb +135 -0
- data/lib/automation_helpers/drivers/v4/capabilities.rb +106 -0
- data/lib/automation_helpers/drivers/v4/local.rb +92 -0
- data/lib/automation_helpers/drivers/v4/options.rb +52 -0
- data/lib/automation_helpers/drivers/v4/remote.rb +64 -0
- data/lib/automation_helpers/drivers/v4.rb +7 -0
- data/lib/automation_helpers/drivers.rb +6 -0
- data/lib/automation_helpers/extensions/array.rb +18 -0
- data/lib/automation_helpers/extensions/capybara/node/element.rb +31 -0
- data/lib/automation_helpers/extensions/cucumber/core/test/case.rb +26 -0
- data/lib/automation_helpers/extensions/selenium/webdriver/logs.rb +47 -0
- data/lib/automation_helpers/extensions/string.rb +34 -0
- data/lib/automation_helpers/extensions.rb +7 -0
- data/lib/automation_helpers/logger.rb +21 -0
- data/lib/automation_helpers/patches/base.rb +52 -0
- data/lib/automation_helpers/patches/capybara.rb +27 -0
- data/lib/automation_helpers/patches/parallel_cucumber.rb +86 -0
- data/lib/automation_helpers/patches/selenium_logger.rb +24 -0
- data/lib/automation_helpers/patches/selenium_manager.rb +43 -0
- data/lib/automation_helpers/patches/selenium_options.rb +72 -0
- data/lib/automation_helpers/patches.rb +8 -0
- data/lib/automation_helpers/version.rb +5 -0
- data/lib/automation_helpers.rb +68 -0
- metadata +201 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Selenium
|
4
|
+
module WebDriver
|
5
|
+
#
|
6
|
+
# Additional useful methods to extend the Selenium::WebDriver::Logs class with
|
7
|
+
#
|
8
|
+
class Logs
|
9
|
+
# @return [Hash]
|
10
|
+
#
|
11
|
+
# Identical to the regular log fetch from Selenium, but stores the logs inside a Hash to be writable later
|
12
|
+
def get(type)
|
13
|
+
cached_logs[type] = @bridge.log(type)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Integer]
|
17
|
+
#
|
18
|
+
# Write the logs to a filepath (If provided), or default file path (Configured). Returns the filesize
|
19
|
+
# as per the regular Ruby Filesystem method
|
20
|
+
def write_log_to_file(type, file = default_file_path)
|
21
|
+
get(type) unless cached_logs.key?(type)
|
22
|
+
|
23
|
+
cached_logs[type].each do |log_entry|
|
24
|
+
if file == $stdout
|
25
|
+
file << log_entry
|
26
|
+
else
|
27
|
+
File.write(file, log_entry)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def default_file_path
|
35
|
+
AutomationHelpers.chrome_log_path || raise(missing_file_path_error)
|
36
|
+
end
|
37
|
+
|
38
|
+
def cached_logs
|
39
|
+
@cached_logs ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def missing_file_path_error
|
43
|
+
'Set the path to store logs using AutomationHelpers.chrome_log_path= or pass in a filepath directly to #write_log_to_file'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Additional useful methods to extend the String class with
|
4
|
+
class String
|
5
|
+
# @return [String]
|
6
|
+
#
|
7
|
+
# Convert's a regular string name into it's pascalized format
|
8
|
+
# This can then be used to generate a ClassName
|
9
|
+
def pascalize
|
10
|
+
split('_').map(&:capitalize).join
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [String]
|
14
|
+
#
|
15
|
+
# Convert's a regular string into a snake cased format
|
16
|
+
# Will sanitize out some characters (Designed for titles)
|
17
|
+
def snake_case
|
18
|
+
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
19
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
20
|
+
.squeeze(' ')
|
21
|
+
.tr('-', '_')
|
22
|
+
.tr(' ', '_')
|
23
|
+
.tr("'", '')
|
24
|
+
.downcase
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String]
|
28
|
+
#
|
29
|
+
# Sanitize and convert every individual whitespace character to
|
30
|
+
# a regular space character (Does not sanitize newlines)
|
31
|
+
def sanitize_whitespace
|
32
|
+
gsub(/[ \t\r\f\u00A0]/, ' ')
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'automation_helpers/extensions/capybara/node/element'
|
4
|
+
require 'automation_helpers/extensions/cucumber/core/test/case'
|
5
|
+
require 'automation_helpers/extensions/selenium/webdriver/logs'
|
6
|
+
require 'automation_helpers/extensions/array'
|
7
|
+
require 'automation_helpers/extensions/string'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module AutomationHelpers
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
#
|
9
|
+
class Logger
|
10
|
+
def self.create(output = $stdout)
|
11
|
+
logger = ::Logger.new(output)
|
12
|
+
logger.progname = 'Automation Helpers'
|
13
|
+
logger.level = :INFO
|
14
|
+
logger.formatter = proc do |severity, time, progname, msg|
|
15
|
+
"#{time.strftime('%F %T')} - #{severity} - #{progname} - #{msg}\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
logger
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutomationHelpers
|
4
|
+
module Patches
|
5
|
+
class Base
|
6
|
+
#
|
7
|
+
# api private (Not intended to be instantiated directly!)
|
8
|
+
#
|
9
|
+
def patch!
|
10
|
+
raise 'This is no longer supported' if prevent_usage?
|
11
|
+
|
12
|
+
Kernel.warn('This is now deprecated and should not be used') if deprecate?
|
13
|
+
AutomationHelpers.logger.info("Adding patch: #{self.class}")
|
14
|
+
AutomationHelpers.logger.debug(description)
|
15
|
+
perform
|
16
|
+
AutomationHelpers.logger.info('Patch successfully added.')
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
#
|
22
|
+
# api private (Not intended to be instantiated directly!)
|
23
|
+
#
|
24
|
+
# From what point should this patch start throwing deprecation notices
|
25
|
+
# If a date is provided, then after that date
|
26
|
+
# If a version from is provided, then all releases after that one (NB: You must provide a link to the gem version)
|
27
|
+
#
|
28
|
+
def deprecate?
|
29
|
+
if defined?(deprecation_notice_date)
|
30
|
+
Time.new >= deprecation_notice_date
|
31
|
+
elsif defined?(deprecate_from)
|
32
|
+
Gem::Version.new(gem_version) >= Gem::Version.new(deprecate_from)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# api private (Not intended to be instantiated directly!)
|
38
|
+
#
|
39
|
+
# From what point should this patch start preventing usage deprecation notices
|
40
|
+
# If a date is provided, then after that date
|
41
|
+
# If a version from is provided, then all releases after that one (NB: You must provide a link to the gem version)
|
42
|
+
#
|
43
|
+
def prevent_usage?
|
44
|
+
if defined?(prevent_usage_date)
|
45
|
+
Time.new >= prevent_usage_date
|
46
|
+
elsif defined?(prevent_usage_from)
|
47
|
+
Gem::Version.new(gem_version) >= Gem::Version.new(prevent_usage_from)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutomationHelpers
|
4
|
+
module Patches
|
5
|
+
class Capybara < Base
|
6
|
+
private
|
7
|
+
|
8
|
+
def description
|
9
|
+
<<~DESCRIPTION
|
10
|
+
There is a bug in the #text method as Safari's w3c conformant endpoint isn't w3c conformant.
|
11
|
+
|
12
|
+
See https://github.com/teamcapybara/capybara/issues/2213 for more details.
|
13
|
+
DESCRIPTION
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform
|
17
|
+
::Capybara::Node::Element.prepend SafariTextPatch
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module SafariTextPatch
|
22
|
+
def text(type = nil, normalize_ws: ::Capybara.default_normalize_ws)
|
23
|
+
super(type, normalize_ws: normalize_ws)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutomationHelpers
|
4
|
+
module Patches
|
5
|
+
class ParallelCucumber < Base
|
6
|
+
private
|
7
|
+
|
8
|
+
def description
|
9
|
+
<<~DESCRIPTION
|
10
|
+
This patch fixes an issue where the parallel tests gem won't place the --retry flag in the correct
|
11
|
+
position for command invocation with cucumber
|
12
|
+
See: https://github.com/grosser/parallel_tests/pull/827 for more details/discussion including this fix
|
13
|
+
DESCRIPTION
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform
|
17
|
+
::ParallelTests::Gherkin::Runner.extend RetryFlagFix
|
18
|
+
end
|
19
|
+
|
20
|
+
def deprecation_notice_date
|
21
|
+
Time.new(2022, 6, 30)
|
22
|
+
end
|
23
|
+
|
24
|
+
def prevent_usage_from
|
25
|
+
'4.0.0'
|
26
|
+
end
|
27
|
+
|
28
|
+
def gem_version
|
29
|
+
::ParallelTests::VERSION
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module RetryFlagFix
|
34
|
+
def run_tests(test_files, process_number, num_processes, options)
|
35
|
+
# Copied Code - https://github.com/grosser/parallel_tests/blob/master/lib/parallel_tests/gherkin/runner.rb#L9
|
36
|
+
combined_scenarios = test_files
|
37
|
+
|
38
|
+
if options[:group_by] == :scenarios
|
39
|
+
grouped = test_files.map { |t| t.split(':') }.group_by(&:first)
|
40
|
+
combined_scenarios = grouped.map do |file, files_and_lines|
|
41
|
+
"#{file}:#{files_and_lines.map(&:last).join(':')}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
sanitized_test_files = combined_scenarios.map { |val| WINDOWS ? "\"#{val}\"" : Shellwords.escape(val) }
|
46
|
+
|
47
|
+
options[:env] ||= {}
|
48
|
+
options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty?
|
49
|
+
|
50
|
+
# New code
|
51
|
+
opts = cucumber_opts(options[:test_options])
|
52
|
+
cmd = [
|
53
|
+
executable,
|
54
|
+
(runtime_logging if File.directory?(File.dirname(runtime_log))),
|
55
|
+
opts[0],
|
56
|
+
*sanitized_test_files,
|
57
|
+
opts[1]
|
58
|
+
].compact.reject(&:empty?).join(' ')
|
59
|
+
execute_command(cmd, process_number, num_processes, options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def cucumber_opts(given)
|
63
|
+
# All new code
|
64
|
+
initial =
|
65
|
+
if given =~ (/--profile/) || given =~ (/(^|\s)-p /)
|
66
|
+
given
|
67
|
+
else
|
68
|
+
[given, profile_from_config].compact.join(" ")
|
69
|
+
end
|
70
|
+
|
71
|
+
opts_as_individuals = initial.scan(/\S+\s\S+/)
|
72
|
+
desired_output = ['', '']
|
73
|
+
|
74
|
+
opts_as_individuals.each do |opt|
|
75
|
+
if opt.match?(/--retry \d+/)
|
76
|
+
desired_output[1] = opt
|
77
|
+
else
|
78
|
+
desired_output[0] = "#{desired_output[0]} #{opt}".strip
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
desired_output
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutomationHelpers
|
4
|
+
module Patches
|
5
|
+
class SeleniumLogger < Base
|
6
|
+
private
|
7
|
+
|
8
|
+
def description
|
9
|
+
<<~DESCRIPTION
|
10
|
+
When using the Selenium Logger that is set to pipe to a file, the Net::HTTP adapter (default),
|
11
|
+
can return unencoded binary (confusingly called ASCII-8BIT in Ruby).
|
12
|
+
Consequently we set the logger to binmode so it doesn't try to encode the data - this would always
|
13
|
+
cause errors for non-ASCII characters, whatever the parent encoding is. An example of this is ©.
|
14
|
+
|
15
|
+
See https://github.com/ruby/net-http/issues/14 for a root cause analysis of the Adapter
|
16
|
+
DESCRIPTION
|
17
|
+
end
|
18
|
+
|
19
|
+
def perform
|
20
|
+
::Selenium::WebDriver.logger.io.binmode
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutomationHelpers
|
4
|
+
module Patches
|
5
|
+
class SeleniumManager < Base
|
6
|
+
private
|
7
|
+
|
8
|
+
def description
|
9
|
+
<<~DESCRIPTION
|
10
|
+
This patch fixes an issue with Selenium4 not bubbling up the httpOnly property of a cookie
|
11
|
+
See: https://github.com/SeleniumHQ/selenium/pull/8958 for more details/discussion including this fix
|
12
|
+
DESCRIPTION
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform
|
16
|
+
::Selenium::WebDriver::Manager.prepend CookieConverter
|
17
|
+
end
|
18
|
+
|
19
|
+
def deprecate_from
|
20
|
+
'4.0.0.beta3'
|
21
|
+
end
|
22
|
+
|
23
|
+
def prevent_usage_from
|
24
|
+
'4.0.0.beta5'
|
25
|
+
end
|
26
|
+
|
27
|
+
def gem_version
|
28
|
+
::Selenium::WebDriver::VERSION
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module CookieConverter
|
33
|
+
def convert_cookie(cookie)
|
34
|
+
super(cookie)
|
35
|
+
.merge(
|
36
|
+
{
|
37
|
+
http_only: cookie['httpOnly']
|
38
|
+
}
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AutomationHelpers
|
4
|
+
module Patches
|
5
|
+
class SeleniumOptions < Base
|
6
|
+
def initialize(browser)
|
7
|
+
@browser = browser
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [Nil || AutomationHelpers.logger.info]
|
12
|
+
#
|
13
|
+
# For SeleniumOptions we only want to run the patch when
|
14
|
+
# we are on browsers without the relevant JSON fixes in upstream
|
15
|
+
def patch!
|
16
|
+
return unless valid?
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def valid?
|
24
|
+
%i[firefox safari].include?(@browser)
|
25
|
+
end
|
26
|
+
|
27
|
+
def description
|
28
|
+
<<~DESCRIPTION
|
29
|
+
This patch fixes an issue with Selenium4 not camelising the browser_name property
|
30
|
+
The issue is the driver, which is now fully W3C conformant expects `browserName`
|
31
|
+
|
32
|
+
See: https://github.com/SeleniumHQ/selenium/pull/8834 for more details/discussion including this fix
|
33
|
+
LH - Nov 2020
|
34
|
+
DESCRIPTION
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform
|
38
|
+
case @browser
|
39
|
+
when :firefox then ::Selenium::WebDriver::Firefox::Options.include CapabilitiesAsJsonFix
|
40
|
+
when :safari then ::Selenium::WebDriver::Safari::Options.include CapabilitiesAsJsonFix
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def deprecate_from
|
45
|
+
'4.0.0.beta5'
|
46
|
+
end
|
47
|
+
|
48
|
+
def prevent_usage_from
|
49
|
+
'4.0.1'
|
50
|
+
end
|
51
|
+
|
52
|
+
def gem_version
|
53
|
+
::Selenium::WebDriver::VERSION
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module CapabilitiesAsJsonFix
|
58
|
+
private
|
59
|
+
|
60
|
+
def generate_as_json(value, camelize_keys: true)
|
61
|
+
if value.is_a?(Hash)
|
62
|
+
value.each_with_object({}) do |(key, val), hash|
|
63
|
+
key = convert_json_key(key, camelize: camelize_keys)
|
64
|
+
hash[key] = generate_as_json(val, camelize_keys: key != 'prefs')
|
65
|
+
end
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'automation_helpers/patches/base'
|
4
|
+
require 'automation_helpers/patches/capybara'
|
5
|
+
require 'automation_helpers/patches/parallel_cucumber'
|
6
|
+
require 'automation_helpers/patches/selenium_logger'
|
7
|
+
require 'automation_helpers/patches/selenium_manager'
|
8
|
+
require 'automation_helpers/patches/selenium_options'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'automation_helpers/drivers'
|
4
|
+
require 'automation_helpers/extensions'
|
5
|
+
require 'automation_helpers/logger'
|
6
|
+
require 'automation_helpers/patches'
|
7
|
+
require 'automation_helpers/version'
|
8
|
+
|
9
|
+
# {AutomationHelpers} namespace
|
10
|
+
module AutomationHelpers
|
11
|
+
class << self
|
12
|
+
attr_accessor :chrome_log_path
|
13
|
+
|
14
|
+
def configure
|
15
|
+
yield self
|
16
|
+
end
|
17
|
+
|
18
|
+
# The Automation Helpers logger object - This is called automatically in several
|
19
|
+
# locations and will log messages according to the normal Ruby protocol
|
20
|
+
# To alter (or check), the log level; call .log_level= or .log_level
|
21
|
+
#
|
22
|
+
# This logger object can also be used to manually log messages
|
23
|
+
#
|
24
|
+
# To Manually log a message
|
25
|
+
# AutomationHelpers.logger.info('Information')
|
26
|
+
# AutomationHelpers.logger.debug('Input debug message')
|
27
|
+
#
|
28
|
+
# By default the logger will output all messages to $stdout, but can be
|
29
|
+
# altered to log to a file or another IO location by calling `.log_path=`
|
30
|
+
def logger
|
31
|
+
@logger ||= Logger.create
|
32
|
+
end
|
33
|
+
|
34
|
+
def logger=(logger)
|
35
|
+
raise ArgumentError, 'You must supply an existing Logger' unless logger.is_a?(::Logger)
|
36
|
+
|
37
|
+
@logger = logger
|
38
|
+
end
|
39
|
+
|
40
|
+
# This writer method allows you to configure where you want the output of
|
41
|
+
# the automation_helpers logs to go (Default is $stdout)
|
42
|
+
#
|
43
|
+
# example: AutomationHelpers.log_path = 'automation_helpers.log' would save all
|
44
|
+
# log messages to `./automation_helpers.log`
|
45
|
+
def log_path=(logdev)
|
46
|
+
logger.reopen(logdev)
|
47
|
+
end
|
48
|
+
|
49
|
+
# To enable full logging (This uses the Ruby API, so can accept any of a
|
50
|
+
# Symbol / String / Integer as an input
|
51
|
+
# AutomationHelpers.log_level = :DEBUG
|
52
|
+
# AutomationHelpers.log_level = 'DEBUG'
|
53
|
+
# AutomationHelpers.log_level = 0
|
54
|
+
#
|
55
|
+
# To disable all logging
|
56
|
+
# AutomationHelpers.log_level = :UNKNOWN
|
57
|
+
def log_level=(value)
|
58
|
+
logger.level = value
|
59
|
+
end
|
60
|
+
|
61
|
+
# To query what level is being logged
|
62
|
+
# AutomationHelpers.log_level
|
63
|
+
# => :INFO # By default
|
64
|
+
def log_level
|
65
|
+
%i[DEBUG INFO WARN ERROR FATAL UNKNOWN][logger.level]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|