bugsnag-maze-runner 6.27.0

Sign up to get free protection for your applications and to get access to all the features.
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