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.
- checksums.yaml +7 -0
- data/bin/bugsnag-print-load-paths +6 -0
- data/bin/download-logs +76 -0
- data/bin/maze-runner +136 -0
- data/bin/upload-app +56 -0
- data/lib/features/scripts/await-android-emulator.sh +11 -0
- data/lib/features/scripts/clear-android-app-data.sh +8 -0
- data/lib/features/scripts/force-stop-android-app.sh +8 -0
- data/lib/features/scripts/install-android-app.sh +15 -0
- data/lib/features/scripts/launch-android-app.sh +38 -0
- data/lib/features/scripts/launch-android-emulator.sh +15 -0
- data/lib/features/steps/android_steps.rb +51 -0
- data/lib/features/steps/app_automator_steps.rb +228 -0
- data/lib/features/steps/aws_sam_steps.rb +212 -0
- data/lib/features/steps/breadcrumb_steps.rb +50 -0
- data/lib/features/steps/browser_steps.rb +93 -0
- data/lib/features/steps/build_api_steps.rb +25 -0
- data/lib/features/steps/document_server_steps.rb +7 -0
- data/lib/features/steps/error_reporting_steps.rb +342 -0
- data/lib/features/steps/feature_flag_steps.rb +190 -0
- data/lib/features/steps/header_steps.rb +72 -0
- data/lib/features/steps/log_steps.rb +29 -0
- data/lib/features/steps/multipart_request_steps.rb +142 -0
- data/lib/features/steps/network_steps.rb +75 -0
- data/lib/features/steps/payload_steps.rb +234 -0
- data/lib/features/steps/proxy_steps.rb +34 -0
- data/lib/features/steps/query_parameter_steps.rb +31 -0
- data/lib/features/steps/request_assertion_steps.rb +107 -0
- data/lib/features/steps/runner_steps.rb +406 -0
- data/lib/features/steps/session_tracking_steps.rb +116 -0
- data/lib/features/steps/value_steps.rb +119 -0
- data/lib/features/support/env.rb +7 -0
- data/lib/features/support/internal_hooks.rb +260 -0
- data/lib/maze/appium_server.rb +112 -0
- data/lib/maze/assertions/request_set_assertions.rb +97 -0
- data/lib/maze/aws/sam.rb +112 -0
- data/lib/maze/bitbar_devices.rb +84 -0
- data/lib/maze/bitbar_utils.rb +112 -0
- data/lib/maze/browser_stack_devices.rb +160 -0
- data/lib/maze/browser_stack_utils.rb +164 -0
- data/lib/maze/browsers_bs.yml +220 -0
- data/lib/maze/browsers_cbt.yml +100 -0
- data/lib/maze/bugsnag_config.rb +42 -0
- data/lib/maze/capabilities.rb +126 -0
- data/lib/maze/checks/assert_check.rb +91 -0
- data/lib/maze/checks/noop_check.rb +34 -0
- data/lib/maze/compare.rb +161 -0
- data/lib/maze/configuration.rb +174 -0
- data/lib/maze/docker.rb +108 -0
- data/lib/maze/document_server.rb +46 -0
- data/lib/maze/driver/appium.rb +217 -0
- data/lib/maze/driver/browser.rb +138 -0
- data/lib/maze/driver/resilient_appium.rb +51 -0
- data/lib/maze/errors.rb +20 -0
- data/lib/maze/helper.rb +118 -0
- data/lib/maze/hooks/appium_hooks.rb +216 -0
- data/lib/maze/hooks/browser_hooks.rb +68 -0
- data/lib/maze/hooks/command_hooks.rb +9 -0
- data/lib/maze/hooks/hooks.rb +61 -0
- data/lib/maze/interactive_cli.rb +173 -0
- data/lib/maze/logger.rb +73 -0
- data/lib/maze/macos_utils.rb +14 -0
- data/lib/maze/network.rb +49 -0
- data/lib/maze/option/parser.rb +245 -0
- data/lib/maze/option/processor.rb +143 -0
- data/lib/maze/option/validator.rb +184 -0
- data/lib/maze/option.rb +64 -0
- data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
- data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
- data/lib/maze/plugins/global_retry_plugin.rb +38 -0
- data/lib/maze/proxy.rb +114 -0
- data/lib/maze/request_list.rb +82 -0
- data/lib/maze/retry_handler.rb +76 -0
- data/lib/maze/runner.rb +149 -0
- data/lib/maze/sauce_labs_utils.rb +96 -0
- data/lib/maze/server.rb +207 -0
- data/lib/maze/servlets/base_servlet.rb +22 -0
- data/lib/maze/servlets/command_servlet.rb +44 -0
- data/lib/maze/servlets/log_servlet.rb +64 -0
- data/lib/maze/servlets/reflective_servlet.rb +69 -0
- data/lib/maze/servlets/servlet.rb +160 -0
- data/lib/maze/smart_bear_utils.rb +71 -0
- data/lib/maze/store.rb +15 -0
- data/lib/maze/terminating_server.rb +129 -0
- data/lib/maze/timers.rb +51 -0
- data/lib/maze/wait.rb +35 -0
- data/lib/maze.rb +27 -0
- 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
|
data/lib/maze/docker.rb
ADDED
@@ -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
|