bugsnag-maze-runner 6.27.0

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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +76 -0
  4. data/bin/maze-runner +136 -0
  5. data/bin/upload-app +56 -0
  6. data/lib/features/scripts/await-android-emulator.sh +11 -0
  7. data/lib/features/scripts/clear-android-app-data.sh +8 -0
  8. data/lib/features/scripts/force-stop-android-app.sh +8 -0
  9. data/lib/features/scripts/install-android-app.sh +15 -0
  10. data/lib/features/scripts/launch-android-app.sh +38 -0
  11. data/lib/features/scripts/launch-android-emulator.sh +15 -0
  12. data/lib/features/steps/android_steps.rb +51 -0
  13. data/lib/features/steps/app_automator_steps.rb +228 -0
  14. data/lib/features/steps/aws_sam_steps.rb +212 -0
  15. data/lib/features/steps/breadcrumb_steps.rb +50 -0
  16. data/lib/features/steps/browser_steps.rb +93 -0
  17. data/lib/features/steps/build_api_steps.rb +25 -0
  18. data/lib/features/steps/document_server_steps.rb +7 -0
  19. data/lib/features/steps/error_reporting_steps.rb +342 -0
  20. data/lib/features/steps/feature_flag_steps.rb +190 -0
  21. data/lib/features/steps/header_steps.rb +72 -0
  22. data/lib/features/steps/log_steps.rb +29 -0
  23. data/lib/features/steps/multipart_request_steps.rb +142 -0
  24. data/lib/features/steps/network_steps.rb +75 -0
  25. data/lib/features/steps/payload_steps.rb +234 -0
  26. data/lib/features/steps/proxy_steps.rb +34 -0
  27. data/lib/features/steps/query_parameter_steps.rb +31 -0
  28. data/lib/features/steps/request_assertion_steps.rb +107 -0
  29. data/lib/features/steps/runner_steps.rb +406 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/value_steps.rb +119 -0
  32. data/lib/features/support/env.rb +7 -0
  33. data/lib/features/support/internal_hooks.rb +260 -0
  34. data/lib/maze/appium_server.rb +112 -0
  35. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  36. data/lib/maze/aws/sam.rb +112 -0
  37. data/lib/maze/bitbar_devices.rb +84 -0
  38. data/lib/maze/bitbar_utils.rb +112 -0
  39. data/lib/maze/browser_stack_devices.rb +160 -0
  40. data/lib/maze/browser_stack_utils.rb +164 -0
  41. data/lib/maze/browsers_bs.yml +220 -0
  42. data/lib/maze/browsers_cbt.yml +100 -0
  43. data/lib/maze/bugsnag_config.rb +42 -0
  44. data/lib/maze/capabilities.rb +126 -0
  45. data/lib/maze/checks/assert_check.rb +91 -0
  46. data/lib/maze/checks/noop_check.rb +34 -0
  47. data/lib/maze/compare.rb +161 -0
  48. data/lib/maze/configuration.rb +174 -0
  49. data/lib/maze/docker.rb +108 -0
  50. data/lib/maze/document_server.rb +46 -0
  51. data/lib/maze/driver/appium.rb +217 -0
  52. data/lib/maze/driver/browser.rb +138 -0
  53. data/lib/maze/driver/resilient_appium.rb +51 -0
  54. data/lib/maze/errors.rb +20 -0
  55. data/lib/maze/helper.rb +118 -0
  56. data/lib/maze/hooks/appium_hooks.rb +216 -0
  57. data/lib/maze/hooks/browser_hooks.rb +68 -0
  58. data/lib/maze/hooks/command_hooks.rb +9 -0
  59. data/lib/maze/hooks/hooks.rb +61 -0
  60. data/lib/maze/interactive_cli.rb +173 -0
  61. data/lib/maze/logger.rb +73 -0
  62. data/lib/maze/macos_utils.rb +14 -0
  63. data/lib/maze/network.rb +49 -0
  64. data/lib/maze/option/parser.rb +245 -0
  65. data/lib/maze/option/processor.rb +143 -0
  66. data/lib/maze/option/validator.rb +184 -0
  67. data/lib/maze/option.rb +64 -0
  68. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  69. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  70. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  71. data/lib/maze/proxy.rb +114 -0
  72. data/lib/maze/request_list.rb +82 -0
  73. data/lib/maze/retry_handler.rb +76 -0
  74. data/lib/maze/runner.rb +149 -0
  75. data/lib/maze/sauce_labs_utils.rb +96 -0
  76. data/lib/maze/server.rb +207 -0
  77. data/lib/maze/servlets/base_servlet.rb +22 -0
  78. data/lib/maze/servlets/command_servlet.rb +44 -0
  79. data/lib/maze/servlets/log_servlet.rb +64 -0
  80. data/lib/maze/servlets/reflective_servlet.rb +69 -0
  81. data/lib/maze/servlets/servlet.rb +160 -0
  82. data/lib/maze/smart_bear_utils.rb +71 -0
  83. data/lib/maze/store.rb +15 -0
  84. data/lib/maze/terminating_server.rb +129 -0
  85. data/lib/maze/timers.rb +51 -0
  86. data/lib/maze/wait.rb +35 -0
  87. data/lib/maze.rb +27 -0
  88. metadata +371 -0
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # MazeRunner configuration
5
+ class Configuration
6
+
7
+ # Set default values
8
+ def initialize
9
+ self.receive_no_requests_wait = 30
10
+ self.receive_requests_wait = 30
11
+ self.enforce_bugsnag_integrity = true
12
+ self.captured_invalid_requests = Set[:errors, :sessions, :builds, :uploads, :sourcemaps]
13
+ end
14
+
15
+ #
16
+ # Server configuration
17
+ #
18
+
19
+ # Mock server bind address
20
+ attr_accessor :bind_address
21
+
22
+ # Mock server port
23
+ attr_accessor :port
24
+
25
+ # Terminating server bind port
26
+ attr_accessor :null_port
27
+
28
+ #
29
+ # Document server configuration
30
+ #
31
+
32
+ # Document server root
33
+ attr_accessor :document_server_root
34
+
35
+ # Document server bind address
36
+ attr_accessor :document_server_bind_address
37
+
38
+ # Document server port
39
+ attr_accessor :document_server_port
40
+
41
+ #
42
+ # Common configuration
43
+ #
44
+
45
+ # Time in seconds to wait in the `I should receive no requests` step
46
+ attr_accessor :receive_no_requests_wait
47
+
48
+ # Maximum time in seconds to wait in the `I wait to receive {int} error(s)/session(s)/build(s)` steps
49
+ attr_accessor :receive_requests_wait
50
+
51
+ # Whether presence of the Bugsnag-Integrity header should be enforced
52
+ attr_accessor :enforce_bugsnag_integrity
53
+
54
+ # Whether retries should be allowed
55
+ attr_accessor :enable_retries
56
+
57
+ # Enables bugsnag reporting
58
+ attr_accessor :enable_bugsnag
59
+
60
+ # The server endpoints for which invalid requests should be captured and cause tests to fail
61
+ attr_accessor :captured_invalid_requests
62
+
63
+ #
64
+ # General appium configuration
65
+ #
66
+
67
+ # Whether each scenario should have its own Appium session
68
+ attr_accessor :appium_session_isolation
69
+
70
+ # Element locator strategy, :id or :accessibility_id
71
+ attr_accessor :locator
72
+
73
+ # Appium capabilities
74
+ attr_accessor :capabilities
75
+
76
+ # Appium capabilities provided via the CL
77
+ attr_accessor :capabilities_option
78
+
79
+ # The app that tests will be run against. Could be one of:
80
+ # - a local file path
81
+ # - a BrowserStack url for a previously uploaded app (bs://...)
82
+ # - on macOS, the name of an installed or previously executed application
83
+ attr_accessor :app
84
+
85
+ # Whether the ResilientAppium driver should be used (only applicable when using Appium in the first place)
86
+ attr_accessor :resilient
87
+
88
+ # Device farm to be used, one of:
89
+ # :cbt (CrossBrowserTesting)
90
+ # :bs (BrowserStack)
91
+ # :local (Using Appium Server with a local device)
92
+ # :none (Cucumber-driven testing with no devices)
93
+ attr_accessor :farm
94
+
95
+ #
96
+ # Device farm specific configuration
97
+ #
98
+
99
+ # Location of the SmartBear binary (if used)
100
+ attr_accessor :sb_local
101
+
102
+ # Location of the BrowserStackLocal binary (if used)
103
+ attr_accessor :bs_local
104
+
105
+ # Location of the Sauce Connect binary (if used)
106
+ attr_accessor :sl_local
107
+
108
+ # Bundle ID of the test application
109
+ attr_accessor :app_bundle_id
110
+
111
+ # Farm username
112
+ attr_accessor :username
113
+
114
+ # Farm access key
115
+ attr_accessor :access_key
116
+
117
+ # Test device type
118
+ attr_accessor :device
119
+
120
+ # A list of devices to attempt to connect to, in order
121
+ attr_accessor :device_list
122
+
123
+ # Test browser type
124
+ attr_accessor :browser
125
+
126
+ # Appium version to use
127
+ attr_accessor :appium_version
128
+
129
+ # URI of the test-management service
130
+ attr_accessor :tms_uri
131
+
132
+ # Access token for the test-management service
133
+ attr_accessor :tms_token
134
+
135
+
136
+ #
137
+ # Local testing specific configuration
138
+ #
139
+
140
+ # Apple Team Id
141
+ attr_accessor :apple_team_id
142
+
143
+ # OS
144
+ attr_accessor :os
145
+
146
+ # OS version
147
+ attr_accessor :os_version
148
+
149
+ # Device id for running on local iOS devices
150
+ attr_accessor :device_id
151
+
152
+ # URL of the Appium server
153
+ attr_accessor :appium_server_url
154
+
155
+ # Whether an appium server should be started
156
+ attr_accessor :start_appium
157
+
158
+ # The location of the appium server logfile
159
+ attr_accessor :appium_logfile
160
+
161
+ #
162
+ # Logging configuration
163
+ #
164
+
165
+ # Write received requests to disk for all scenarios
166
+ attr_accessor :file_log
167
+
168
+ # Console logging of received requests for a test failure
169
+ attr_accessor :log_requests
170
+
171
+ # Always log all received requests to the console at the end of a scenario
172
+ attr_accessor :always_log
173
+ end
174
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runner'
4
+
5
+ module Maze
6
+ # Responsible for running docker containers in the local environment
7
+ class Docker
8
+ class << self
9
+ # The default place to look for the docker-compose file
10
+ COMPOSE_FILENAME = 'features/fixtures/docker-compose.yml'
11
+
12
+ # @!attribute [a] last_exit_code Provides access to the exit code of the last run docker command
13
+ attr_accessor :last_exit_code
14
+
15
+ # @!attribute [a] last_command_logs Provides access to the output from the last run docker command
16
+ attr_accessor :last_command_logs
17
+
18
+ # Builds and starts a service, using a command if given.
19
+ # If running a command, it will be executed as an attached process, otherwise it
20
+ # will run detached.
21
+ #
22
+ # @param service [String] The name of the service to start
23
+ # @param command [String] Optional. The command to use when running the service
24
+ # @param interactive [Boolean] Optional. Whether to run interactively
25
+ def start_service(service, command: nil, interactive: false)
26
+ if interactive
27
+ run_docker_compose_command("build #{service}")
28
+
29
+ # Run the built service in an interactive session. The service _must_
30
+ # have an appropriate entrypoint, e.g. '/bin/sh'. We also disable ANSI
31
+ # escape sequences from docker-compose as they can cause issues with
32
+ # stderr expectations by 'leaking' into the next line
33
+ command = get_docker_compose_command("--no-ansi run #{service} #{command}")
34
+
35
+ cli = Runner.start_interactive_session(command)
36
+ cli.on_exit do |status|
37
+ @last_exit_code = status
38
+ @last_command_logs = cli.stdout_lines + cli.stderr_lines
39
+ end
40
+
41
+ # The logs and exit code aren't available from the interactive session
42
+ # at this point (we've just started it!) so we can't provide them here
43
+ @last_command_logs = []
44
+ @last_exit_code = nil
45
+ elsif command
46
+ # We build the service before running it as there is no --build
47
+ # option for run.
48
+ run_docker_compose_command("build #{service}")
49
+ run_docker_compose_command("run --use-aliases #{service} #{command}")
50
+ else
51
+ # TODO: Consider adding a logs command here
52
+ run_docker_compose_command("up -d --build #{service}")
53
+ end
54
+ @services_started = true
55
+ end
56
+
57
+ # Kills a running service
58
+ #
59
+ # @param service [String] The name of the service to kill
60
+ def down_service(service)
61
+ # We set timeout to 0 so this kills the services rather than stopping them
62
+ # as its quicker and they are stateless anyway.
63
+ run_docker_compose_command("down -t 0 #{service}")
64
+ end
65
+
66
+ # Resets any state ready for the next scenario
67
+ def reset
68
+ down_all_services
69
+ @last_exit_code = nil
70
+ @last_command_logs = nil
71
+ end
72
+
73
+ # Kills all running services
74
+ def down_all_services
75
+ # This will fail to remove the network that maze is connected to
76
+ # as it is still in use, that is ok to ignore so we pass success codes!
77
+ # We set timeout to 0 so this kills the services rather than stopping them
78
+ # as its quicker and they are stateless anyway.
79
+ run_docker_compose_command('down -t 0', success_codes: [0, 256]) if compose_stack_exists? && @services_started
80
+ @services_started = false
81
+ end
82
+
83
+ def compose_project_name
84
+ @compose_project_name ||= nil
85
+ end
86
+
87
+ attr_writer :compose_project_name
88
+
89
+ private
90
+
91
+ def get_docker_compose_command(command)
92
+ project_name = compose_project_name.nil? ? '' : "-p #{compose_project_name}"
93
+
94
+ "docker-compose #{project_name} -f #{COMPOSE_FILENAME} #{command}"
95
+ end
96
+
97
+ def run_docker_compose_command(command, success_codes: nil)
98
+ command = get_docker_compose_command(command)
99
+
100
+ @last_command_logs, @last_exit_code = Runner.run_command(command, success_codes: success_codes)
101
+ end
102
+
103
+ def compose_stack_exists?
104
+ File.exist? COMPOSE_FILENAME
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webrick'
4
+
5
+ module Maze
6
+ # HTTP server for a given document root
7
+ class DocumentServer
8
+ class << self
9
+ # Start the document server. This is intended to be called only once per test run.
10
+ # Use manual_start for finer grained control.
11
+ def start
12
+ @thread = Thread.new do
13
+ options = {
14
+ DocumentRoot: Maze.config.document_server_root,
15
+ Port: Maze.config.document_server_port,
16
+ Logger: $logger,
17
+ AccessLog: []
18
+ }
19
+ options[:BindAddress] = Maze.config.document_server_bind_address unless Maze.config.document_server_bind_address.nil?
20
+ server = WEBrick::HTTPServer.new(options)
21
+ server.mount '/reflect', Servlets::ReflectiveServlet
22
+
23
+ $logger.info "Starting document server for root: #{Maze.config.document_server_root}"
24
+ server.start
25
+ end
26
+ end
27
+
28
+ # Starts the document server "manually" (via a Cucumber step as opposed to command line option)
29
+ def manual_start
30
+ if !@thread.nil? && @thread.alive?
31
+ $logger.warn 'Document Server has already been started on the command line, ignoring manual start'
32
+ return
33
+ end
34
+ @manual_start = true
35
+ start
36
+ end
37
+
38
+ def manual_stop
39
+ return unless @manual_start
40
+
41
+ @thread.kill
42
+ @manual_start = false
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,217 @@
1
+ require 'appium_lib'
2
+ require 'json'
3
+ require 'open3'
4
+ require 'securerandom'
5
+ require_relative '../logger'
6
+ require_relative '../../maze'
7
+
8
+ module Maze
9
+ module Driver
10
+ # Provide a thin layer of abstraction above @see Appium::Driver
11
+ class Appium < Appium::Driver
12
+
13
+ # @!attribute [r] device_type
14
+ # @return [String] The device, from the list of device capabilities, used for this test
15
+ attr_reader :device_type
16
+
17
+ # @!attribute [r] capabilities
18
+ # @return [Hash] The capabilities used to launch the BrowserStack instance
19
+ attr_reader :capabilities
20
+
21
+ # Creates the Appium driver
22
+ #
23
+ # @param server_url [String] URL of the Appium server
24
+ # @param capabilities [Hash] a hash of capabilities to be used in this test run
25
+ # @param locator [Symbol] the primary locator strategy Appium should use to find elements
26
+ def initialize(server_url, capabilities, locator = :id)
27
+ # Sets up identifiers for ease of connecting jobs
28
+ name_capabilities = project_name_capabilities
29
+
30
+ @element_locator = locator
31
+ @capabilities = capabilities
32
+ @capabilities.merge! name_capabilities
33
+
34
+ # Timers
35
+ @find_element_timer = Maze.timers.add 'Appium - find element'
36
+ @click_element_timer = Maze.timers.add 'Appium - click element'
37
+ @clear_element_timer = Maze.timers.add 'Appium - clear element'
38
+ @send_keys_timer = Maze.timers.add 'Appium - send keys to element'
39
+
40
+ super({
41
+ 'caps' => @capabilities,
42
+ 'appium_lib' => {
43
+ server_url: server_url
44
+ }
45
+ }, true)
46
+
47
+ $logger.info 'Appium driver initialized for:'
48
+ $logger.info " project : #{name_capabilities[:project]}"
49
+ $logger.info " build : #{name_capabilities[:build]}"
50
+ end
51
+
52
+ # Starts the Appium driver
53
+ def start_driver
54
+ begin
55
+ $logger.info 'Starting Appium driver...'
56
+ time = Time.now
57
+ super
58
+ $logger.info "Appium driver started in #{(Time.now - time).to_i}s"
59
+ rescue => error
60
+ $logger.warn "Appium driver failed to start in #{(Time.now - time).to_i}s"
61
+ $logger.warn "#{error.class} occurred with message: #{error.message}"
62
+ raise error
63
+ end
64
+ end
65
+
66
+ # Checks for an element, waiting until it is present or the method times out
67
+ #
68
+ # @param element_id [String] the element to search for
69
+ # @param timeout [Integer] the maximum time to wait for an element to be present in seconds
70
+ # @param retry_if_stale [Boolean] enables the method to retry acquiring the element if a StaleObjectException occurs
71
+ def wait_for_element(element_id, timeout = 15, retry_if_stale = true)
72
+ wait = Selenium::WebDriver::Wait.new(timeout: timeout)
73
+ wait.until { find_element(@element_locator, element_id).displayed? }
74
+ rescue Selenium::WebDriver::Error::TimeoutError
75
+ false
76
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError => e
77
+ if retry_if_stale
78
+ wait_for_element(element_id, timeout, false)
79
+ else
80
+ $logger.warn "StaleElementReferenceError occurred: #{e}"
81
+ false
82
+ end
83
+ else
84
+ true
85
+ end
86
+
87
+ # A wrapper around find_element adding timer functionality
88
+ def find_element_timed(element_id)
89
+ @find_element_timer.time do
90
+ find_element(@element_locator, element_id)
91
+ end
92
+ end
93
+
94
+ # Clicks a given element
95
+ #
96
+ # @param element_id [String] the element to click
97
+ def click_element(element_id)
98
+ element = find_element_timed(element_id)
99
+ @click_element_timer.time do
100
+ element.click
101
+ end
102
+ end
103
+
104
+ # Clicks a given element, ignoring any NoSuchElementError
105
+ #
106
+ # @param element_id [String] the element to click
107
+ # @return [Boolean] True is the element was clicked
108
+ def click_element_if_present(element_id)
109
+ element = find_element_timed(element_id)
110
+ @click_element_timer.time do
111
+ element.click
112
+ end
113
+ true
114
+ rescue Selenium::WebDriver::Error::NoSuchElementError
115
+ false
116
+ end
117
+
118
+ # Clears a given element
119
+ #
120
+ # @param element_id [String] the element to clear
121
+ def clear_element(element_id)
122
+ element = find_element_timed(element_id)
123
+ @clear_element_timer.time do
124
+ element.clear
125
+ end
126
+ end
127
+
128
+ # Gets the application hierarchy XML
129
+ def page_source
130
+ @driver.page_source
131
+ end
132
+
133
+ # Unlocks the device
134
+ def unlock
135
+ @driver.unlock
136
+ end
137
+
138
+ # Sends keys to a given element
139
+ #
140
+ # @param element_id [String] the element to send text to
141
+ # @param text [String] the text to send
142
+ def send_keys_to_element(element_id, text)
143
+ element = find_element_timed(element_id)
144
+ @send_keys_timer.time do
145
+ element.send_keys(text)
146
+ end
147
+ end
148
+
149
+ # Sets the rotation of the device
150
+ #
151
+ # @param orientation [Symbol] :portrait or :landscape
152
+ def set_rotation(orientation)
153
+ @driver.rotation = orientation
154
+ end
155
+
156
+ def window_size
157
+ @driver.window_size
158
+ end
159
+
160
+ # Send keys to the device without a specific element
161
+ #
162
+ # @param text [String] the text to send
163
+ def send_keys(text)
164
+ @driver.send_keys(text)
165
+ end
166
+
167
+ # Sends keys to a given element, clearing it first
168
+ #
169
+ # @param element_id [String] the element to clear and send text to
170
+ # @param text [String] the text to send
171
+ def clear_and_send_keys_to_element(element_id, text)
172
+ element = find_element_timed(element_id)
173
+ @clear_element_timer.time do
174
+ element.clear
175
+ end
176
+
177
+ @send_keys_timer.time do
178
+ element.send_keys(text)
179
+ end
180
+ end
181
+
182
+ # Reset the currently running application after a given timeout
183
+ #
184
+ # @param timeout [Number] the amount of time in seconds to wait before resetting
185
+ def reset_with_timeout(timeout = 0.1)
186
+ sleep(timeout)
187
+ reset
188
+ end
189
+
190
+ # Determines and returns sensible project, build, and name capabilities
191
+ #
192
+ # @return [Hash] A hash containing the 'project' and 'build' capabilities
193
+ def project_name_capabilities
194
+ # Default to values for running locally
195
+ project = 'local'
196
+ build = SecureRandom.uuid
197
+
198
+ if ENV['BUILDKITE']
199
+ # Project
200
+ project = ENV['BUILDKITE_PIPELINE_NAME']
201
+ end
202
+ {
203
+ project: project,
204
+ build: build
205
+ }
206
+ end
207
+
208
+ def device_info
209
+ driver.execute_script('mobile:deviceInfo')
210
+ end
211
+
212
+ def session_capabilities
213
+ driver.session_capabilities
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'selenium-webdriver'
4
+
5
+ module Maze
6
+ module Driver
7
+ # Handles browser automation fundamentals
8
+ class Browser
9
+ # @!attribute [r] capabilities
10
+ # @return [Hash] The capabilities used to launch the BrowserStack instance
11
+ attr_reader :capabilities
12
+
13
+ def initialize(driver_for, selenium_url=nil, capabilities=nil)
14
+ capabilities.merge! project_name_capabilities
15
+ @capabilities = capabilities
16
+ @driver_for = driver_for
17
+ @selenium_url = selenium_url
18
+
19
+ start_driver
20
+ end
21
+
22
+ def find_element(*args)
23
+ @driver.find_element(*args)
24
+ end
25
+
26
+ def navigate
27
+ @driver.navigate
28
+ end
29
+
30
+ # Refreshes the page
31
+ def refresh
32
+ @driver.refresh
33
+ end
34
+
35
+ # Quits the driver
36
+ def driver_quit
37
+ @driver.quit
38
+ end
39
+
40
+ # check if Selenium supports running javascript in the current browser
41
+ def javascript?
42
+ @driver.execute_script('return true')
43
+ rescue Selenium::WebDriver::Error::UnsupportedOperationError
44
+ false
45
+ end
46
+
47
+ # check if the browser supports local storage, e.g. safari 10 on browserstack
48
+ # does not have working local storage
49
+ def local_storage?
50
+ # Assume we can use local storage if we aren't able to verify by running JavaScript
51
+ return true unless javascript?
52
+
53
+ @driver.execute_script <<-JAVASCRIPT
54
+ try {
55
+ window.localStorage.setItem('__localstorage_test__', 1234)
56
+ window.localStorage.removeItem('__localstorage_test__')
57
+
58
+ return true
59
+ } catch (err) {
60
+ return false
61
+ }
62
+ JAVASCRIPT
63
+ end
64
+
65
+ # Determines and returns sensible project and build capabilities
66
+ #
67
+ # @return [Hash] A hash containing the 'project' and 'build' capabilities
68
+ def project_name_capabilities
69
+ # Default to values for running locally
70
+ project = 'local'
71
+ build = SecureRandom.uuid
72
+
73
+ if ENV['BUILDKITE']
74
+ # Project
75
+ project = ENV['BUILDKITE_PIPELINE_NAME']
76
+ end
77
+ {
78
+ project: project,
79
+ build: build
80
+ }
81
+ end
82
+
83
+ # Restarts the underlying-driver in the case an unrecoverable error occurs
84
+ #
85
+ # @param attempts [Integer] The number of times we should retry a failed attempt (defaults to 6)
86
+ def restart_driver(attempts=6)
87
+ # Remove the old driver
88
+ @driver.quit
89
+ @driver = nil
90
+
91
+ start_driver(attempts)
92
+ end
93
+
94
+ private
95
+
96
+ # Attempts to create a new selenium driver a given number of times
97
+ #
98
+ # @param attempts [Integer] The number of times we should retry a failed attempt (defaults to 6)
99
+ def start_driver(attempts=6)
100
+ timeout = attempts * 10
101
+ wait = Maze::Wait.new(interval: 10, timeout: timeout)
102
+ success = wait.until do
103
+ begin
104
+ create_driver(@driver_for, @selenium_url)
105
+ rescue => error
106
+ $logger.warn "#{error.class} occurred with message: #{error.message}"
107
+ end
108
+ @driver
109
+ end
110
+
111
+ unless success
112
+ $logger.error "Selenium driver failed to start after #{attempts} attempts in #{timeout} seconds"
113
+ raise RuntimeError.new("Selenium driver failed to start in #{timeout} seconds")
114
+ end
115
+ end
116
+
117
+ # Creates and starts the selenium driver
118
+ def create_driver(driver_for, selenium_url=nil)
119
+ begin
120
+ $logger.info "Starting Selenium driver"
121
+ time = Time.now
122
+ if driver_for == :remote
123
+ driver = ::Selenium::WebDriver.for :remote,
124
+ url: selenium_url,
125
+ desired_capabilities: @capabilities
126
+ else
127
+ driver = ::Selenium::WebDriver.for driver_for
128
+ end
129
+ $logger.info "Selenium driver started in #{(Time.now - time).to_i}s"
130
+ @driver = driver
131
+ rescue => error
132
+ $logger.warn "Selenium driver failed to start in #{(Time.now - time).to_i}s"
133
+ raise error
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end