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