automation_helpers 4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +13 -0
  3. data/README.md +49 -0
  4. data/lib/automation_helpers/drivers/browserstack.rb +9 -0
  5. data/lib/automation_helpers/drivers/local.rb +9 -0
  6. data/lib/automation_helpers/drivers/remote.rb +9 -0
  7. data/lib/automation_helpers/drivers/v4/browserstack.rb +135 -0
  8. data/lib/automation_helpers/drivers/v4/capabilities.rb +106 -0
  9. data/lib/automation_helpers/drivers/v4/local.rb +92 -0
  10. data/lib/automation_helpers/drivers/v4/options.rb +52 -0
  11. data/lib/automation_helpers/drivers/v4/remote.rb +64 -0
  12. data/lib/automation_helpers/drivers/v4.rb +7 -0
  13. data/lib/automation_helpers/drivers.rb +6 -0
  14. data/lib/automation_helpers/extensions/array.rb +18 -0
  15. data/lib/automation_helpers/extensions/capybara/node/element.rb +31 -0
  16. data/lib/automation_helpers/extensions/cucumber/core/test/case.rb +26 -0
  17. data/lib/automation_helpers/extensions/selenium/webdriver/logs.rb +47 -0
  18. data/lib/automation_helpers/extensions/string.rb +34 -0
  19. data/lib/automation_helpers/extensions.rb +7 -0
  20. data/lib/automation_helpers/logger.rb +21 -0
  21. data/lib/automation_helpers/patches/base.rb +52 -0
  22. data/lib/automation_helpers/patches/capybara.rb +27 -0
  23. data/lib/automation_helpers/patches/parallel_cucumber.rb +86 -0
  24. data/lib/automation_helpers/patches/selenium_logger.rb +24 -0
  25. data/lib/automation_helpers/patches/selenium_manager.rb +43 -0
  26. data/lib/automation_helpers/patches/selenium_options.rb +72 -0
  27. data/lib/automation_helpers/patches.rb +8 -0
  28. data/lib/automation_helpers/version.rb +5 -0
  29. data/lib/automation_helpers.rb +68 -0
  30. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AutomationHelpers
4
+ VERSION = '4.0'
5
+ end
@@ -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