bugsnag-maze-runner 7.22.1
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 +74 -0
- data/bin/maze-runner +174 -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 +80 -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 +358 -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 +135 -0
- data/lib/features/steps/payload_steps.rb +257 -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 +186 -0
- data/lib/features/steps/runner_steps.rb +428 -0
- data/lib/features/steps/session_tracking_steps.rb +116 -0
- data/lib/features/steps/trace_steps.rb +206 -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 +207 -0
- data/lib/maze/api/appium/file_manager.rb +29 -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/aws_public_ip.rb +53 -0
- data/lib/maze/bugsnag_config.rb +42 -0
- data/lib/maze/checks/assert_check.rb +69 -0
- data/lib/maze/checks/noop_check.rb +34 -0
- data/lib/maze/client/appium/base_client.rb +131 -0
- data/lib/maze/client/appium/bb_client.rb +102 -0
- data/lib/maze/client/appium/bb_devices.rb +127 -0
- data/lib/maze/client/appium/bs_client.rb +91 -0
- data/lib/maze/client/appium/bs_devices.rb +141 -0
- data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
- data/lib/maze/client/appium/local_client.rb +67 -0
- data/lib/maze/client/appium.rb +23 -0
- data/lib/maze/client/bb_api_client.rb +102 -0
- data/lib/maze/client/bb_client_utils.rb +181 -0
- data/lib/maze/client/bs_client_utils.rb +168 -0
- data/lib/maze/client/selenium/base_client.rb +15 -0
- data/lib/maze/client/selenium/bb_browsers.yml +188 -0
- data/lib/maze/client/selenium/bb_client.rb +38 -0
- data/lib/maze/client/selenium/bs_browsers.yml +257 -0
- data/lib/maze/client/selenium/bs_client.rb +89 -0
- data/lib/maze/client/selenium/local_client.rb +16 -0
- data/lib/maze/client/selenium.rb +16 -0
- data/lib/maze/compare.rb +161 -0
- data/lib/maze/configuration.rb +182 -0
- data/lib/maze/docker.rb +147 -0
- data/lib/maze/document_server.rb +46 -0
- data/lib/maze/driver/appium.rb +198 -0
- data/lib/maze/driver/browser.rb +124 -0
- data/lib/maze/errors.rb +52 -0
- data/lib/maze/generator.rb +55 -0
- data/lib/maze/helper.rb +122 -0
- data/lib/maze/hooks/appium_hooks.rb +55 -0
- data/lib/maze/hooks/browser_hooks.rb +15 -0
- data/lib/maze/hooks/command_hooks.rb +9 -0
- data/lib/maze/hooks/error_code_hook.rb +49 -0
- data/lib/maze/hooks/hooks.rb +61 -0
- data/lib/maze/http_request.rb +21 -0
- data/lib/maze/interactive_cli.rb +173 -0
- data/lib/maze/logger.rb +86 -0
- data/lib/maze/macos_utils.rb +14 -0
- data/lib/maze/maze_output.rb +88 -0
- data/lib/maze/network.rb +49 -0
- data/lib/maze/option/parser.rb +240 -0
- data/lib/maze/option/processor.rb +130 -0
- data/lib/maze/option/validator.rb +155 -0
- data/lib/maze/option.rb +62 -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/error_code_plugin.rb +21 -0
- data/lib/maze/plugins/global_retry_plugin.rb +38 -0
- data/lib/maze/proxy.rb +114 -0
- data/lib/maze/request_list.rb +87 -0
- data/lib/maze/request_repeater.rb +49 -0
- data/lib/maze/retry_handler.rb +67 -0
- data/lib/maze/runner.rb +149 -0
- data/lib/maze/schemas/OtelTraceSchema.json +390 -0
- data/lib/maze/schemas/trace_schema.rb +7 -0
- data/lib/maze/schemas/trace_validator.rb +98 -0
- data/lib/maze/server.rb +251 -0
- data/lib/maze/servlets/base_servlet.rb +27 -0
- data/lib/maze/servlets/command_servlet.rb +47 -0
- data/lib/maze/servlets/log_servlet.rb +64 -0
- data/lib/maze/servlets/reflective_servlet.rb +70 -0
- data/lib/maze/servlets/servlet.rb +199 -0
- data/lib/maze/servlets/temp.rb +0 -0
- data/lib/maze/servlets/trace_servlet.rb +13 -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
- data/lib/utils/deep_merge.rb +17 -0
- data/lib/utils/selenium_money_patch.rb +17 -0
- metadata +451 -0
@@ -0,0 +1,182 @@
|
|
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
|
+
@legacy_driver = false
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Server configuration
|
18
|
+
#
|
19
|
+
|
20
|
+
# Mock server bind address
|
21
|
+
attr_accessor :bind_address
|
22
|
+
|
23
|
+
# Mock server port
|
24
|
+
attr_accessor :port
|
25
|
+
|
26
|
+
# Terminating server bind port
|
27
|
+
attr_accessor :null_port
|
28
|
+
|
29
|
+
#
|
30
|
+
# Document server configuration
|
31
|
+
#
|
32
|
+
|
33
|
+
# Document server root
|
34
|
+
attr_accessor :document_server_root
|
35
|
+
|
36
|
+
# Document server bind address
|
37
|
+
attr_accessor :document_server_bind_address
|
38
|
+
|
39
|
+
# Document server port
|
40
|
+
attr_accessor :document_server_port
|
41
|
+
|
42
|
+
#
|
43
|
+
# Common configuration
|
44
|
+
#
|
45
|
+
|
46
|
+
# Time in seconds to wait in the `I should receive no requests` step
|
47
|
+
attr_accessor :receive_no_requests_wait
|
48
|
+
|
49
|
+
# Maximum time in seconds to wait in the `I wait to receive {int} error(s)/session(s)/build(s)` steps
|
50
|
+
attr_accessor :receive_requests_wait
|
51
|
+
|
52
|
+
# Whether presence of the Bugsnag-Integrity header should be enforced
|
53
|
+
attr_accessor :enforce_bugsnag_integrity
|
54
|
+
|
55
|
+
# Whether retries should be allowed
|
56
|
+
attr_accessor :enable_retries
|
57
|
+
|
58
|
+
# Enables bugsnag reporting
|
59
|
+
attr_accessor :enable_bugsnag
|
60
|
+
|
61
|
+
# The server endpoints for which invalid requests should be captured and cause tests to fail
|
62
|
+
attr_accessor :captured_invalid_requests
|
63
|
+
|
64
|
+
# API key to use when repeating requests
|
65
|
+
attr_accessor :repeater_api_key
|
66
|
+
|
67
|
+
# Enables awareness of a public IP address on Buildkite with the Elastic CI Stack for AWS.
|
68
|
+
attr_accessor :aws_public_ip
|
69
|
+
|
70
|
+
#
|
71
|
+
# General appium configuration
|
72
|
+
#
|
73
|
+
|
74
|
+
# Element locator strategy, :id or :accessibility_id
|
75
|
+
attr_accessor :locator
|
76
|
+
|
77
|
+
# Appium capabilities
|
78
|
+
attr_accessor :capabilities
|
79
|
+
|
80
|
+
# Appium capabilities provided via the CL
|
81
|
+
attr_accessor :capabilities_option
|
82
|
+
|
83
|
+
# The app that tests will be run against. Could be one of:
|
84
|
+
# - a local file path
|
85
|
+
# - a BrowserStack url for a previously uploaded app (bs://...)
|
86
|
+
# - on macOS, the name of an installed or previously executed application
|
87
|
+
attr_accessor :app
|
88
|
+
|
89
|
+
# Device farm to be used, one of:
|
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
|
+
# Whether the device farm secure tunnel should be started
|
96
|
+
attr_accessor :start_tunnel
|
97
|
+
|
98
|
+
#
|
99
|
+
# Device farm specific configuration
|
100
|
+
#
|
101
|
+
|
102
|
+
# Location of the SmartBear binary (if used)
|
103
|
+
attr_accessor :sb_local
|
104
|
+
|
105
|
+
# Location of the BrowserStackLocal binary (if used)
|
106
|
+
attr_accessor :bs_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
|
+
# Whether the legacy (JSON-WP) Appium driver should be used
|
136
|
+
def legacy_driver?
|
137
|
+
@legacy_driver
|
138
|
+
end
|
139
|
+
|
140
|
+
def legacy_driver=(value)
|
141
|
+
@legacy_driver = value
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# Local testing specific configuration
|
146
|
+
#
|
147
|
+
|
148
|
+
# Apple Team Id
|
149
|
+
attr_accessor :apple_team_id
|
150
|
+
|
151
|
+
# OS
|
152
|
+
attr_accessor :os
|
153
|
+
|
154
|
+
# OS version
|
155
|
+
attr_accessor :os_version
|
156
|
+
|
157
|
+
# Device id for running on local iOS devices
|
158
|
+
attr_accessor :device_id
|
159
|
+
|
160
|
+
# URL of the Appium server
|
161
|
+
attr_accessor :appium_server_url
|
162
|
+
|
163
|
+
# Whether an appium server should be started
|
164
|
+
attr_accessor :start_appium
|
165
|
+
|
166
|
+
# The location of the appium server logfile
|
167
|
+
attr_accessor :appium_logfile
|
168
|
+
|
169
|
+
#
|
170
|
+
# Logging configuration
|
171
|
+
#
|
172
|
+
|
173
|
+
# Write received requests to disk for all scenarios
|
174
|
+
attr_accessor :file_log
|
175
|
+
|
176
|
+
# Console logging of received requests for a test failure
|
177
|
+
attr_accessor :log_requests
|
178
|
+
|
179
|
+
# Always log all received requests to the console at the end of a scenario
|
180
|
+
attr_accessor :always_log
|
181
|
+
end
|
182
|
+
end
|
data/lib/maze/docker.rb
ADDED
@@ -0,0 +1,147 @@
|
|
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
|
+
# Execute a command in an already running docker container. Use {start_service}
|
58
|
+
# to build and run the service before calling this method
|
59
|
+
#
|
60
|
+
# This is equivalent to the Docker Compose 'exec' command:
|
61
|
+
# https://docs.docker.com/engine/reference/commandline/compose_exec/
|
62
|
+
#
|
63
|
+
# @param service [String] The name of the service. This must already be running
|
64
|
+
# @param command [String] The command to run
|
65
|
+
# @param detach [Boolean] Optional. Whether to run detached
|
66
|
+
def exec(service, command, detach: false)
|
67
|
+
flags = detach ? " --detach" : ""
|
68
|
+
|
69
|
+
run_docker_compose_command("exec #{flags} #{service} /bin/sh -c '#{command}'")
|
70
|
+
end
|
71
|
+
|
72
|
+
# Copy a file or directory from a container to the local filesystem
|
73
|
+
#
|
74
|
+
# This is equivalent to the Docker Compose 'cp' command:
|
75
|
+
# https://docs.docker.com/engine/reference/commandline/compose_cp/
|
76
|
+
#
|
77
|
+
# @param service [String] The name of the service to copy from. This must already be running
|
78
|
+
# @param from [String] The path to the file/directory that should be copied
|
79
|
+
# @param to [String] The path to copy the file/directory to
|
80
|
+
def copy_from_container(service, from:, to:)
|
81
|
+
run_docker_compose_command("cp #{service}:#{from} #{to}", success_codes: [0])
|
82
|
+
end
|
83
|
+
|
84
|
+
# Copy a file or directory from the local filesystem to a container
|
85
|
+
#
|
86
|
+
# This is equivalent to the Docker Compose 'cp' command:
|
87
|
+
# https://docs.docker.com/engine/reference/commandline/compose_cp/
|
88
|
+
#
|
89
|
+
# @param service [String] The name of the service to copy to. This must already be running
|
90
|
+
# @param from [String] The path to the file/directory that should be copied
|
91
|
+
# @param to [String] The path to copy the file/directory to
|
92
|
+
def copy_to_container(service, from:, to:)
|
93
|
+
run_docker_compose_command("cp #{from} #{service}:#{to}", success_codes: [0])
|
94
|
+
end
|
95
|
+
|
96
|
+
# Kills a running service
|
97
|
+
#
|
98
|
+
# @param service [String] The name of the service to kill
|
99
|
+
def down_service(service)
|
100
|
+
# We set timeout to 0 so this kills the services rather than stopping them
|
101
|
+
# as its quicker and they are stateless anyway.
|
102
|
+
run_docker_compose_command("down -t 0 #{service}")
|
103
|
+
end
|
104
|
+
|
105
|
+
# Resets any state ready for the next scenario
|
106
|
+
def reset
|
107
|
+
down_all_services
|
108
|
+
@last_exit_code = nil
|
109
|
+
@last_command_logs = nil
|
110
|
+
end
|
111
|
+
|
112
|
+
# Kills all running services
|
113
|
+
def down_all_services
|
114
|
+
# This will fail to remove the network that maze is connected to
|
115
|
+
# as it is still in use, that is ok to ignore so we pass success codes!
|
116
|
+
# We set timeout to 0 so this kills the services rather than stopping them
|
117
|
+
# as its quicker and they are stateless anyway.
|
118
|
+
run_docker_compose_command('down -t 0', success_codes: [0, 256]) if compose_stack_exists? && @services_started
|
119
|
+
@services_started = false
|
120
|
+
end
|
121
|
+
|
122
|
+
def compose_project_name
|
123
|
+
@compose_project_name ||= nil
|
124
|
+
end
|
125
|
+
|
126
|
+
attr_writer :compose_project_name
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def get_docker_compose_command(command)
|
131
|
+
project_name = compose_project_name.nil? ? '' : "-p #{compose_project_name}"
|
132
|
+
|
133
|
+
"docker compose #{project_name} -f #{COMPOSE_FILENAME} #{command}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def run_docker_compose_command(command, success_codes: nil)
|
137
|
+
command = get_docker_compose_command(command)
|
138
|
+
|
139
|
+
@last_command_logs, @last_exit_code = Runner.run_command(command, success_codes: success_codes)
|
140
|
+
end
|
141
|
+
|
142
|
+
def compose_stack_exists?
|
143
|
+
File.exist? COMPOSE_FILENAME
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
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,198 @@
|
|
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 [rw] app_id
|
14
|
+
# @return [String] The app_id derived from session_capabilities (appPackage on Android, bundleID on iOS)
|
15
|
+
attr_accessor :app_id
|
16
|
+
|
17
|
+
# @!attribute [r] device_type
|
18
|
+
# @return [String] The device, from the list of device capabilities, used for this test
|
19
|
+
attr_reader :device_type
|
20
|
+
|
21
|
+
# @!attribute [r] capabilities
|
22
|
+
# @return [Hash] The capabilities used to launch the BrowserStack instance
|
23
|
+
attr_reader :capabilities
|
24
|
+
|
25
|
+
# Creates the Appium driver
|
26
|
+
#
|
27
|
+
# @param server_url [String] URL of the Appium server
|
28
|
+
# @param capabilities [Hash] a hash of capabilities to be used in this test run
|
29
|
+
# @param locator [Symbol] the primary locator strategy Appium should use to find elements
|
30
|
+
def initialize(server_url, capabilities, locator = :id)
|
31
|
+
# Sets up identifiers for ease of connecting jobs
|
32
|
+
capabilities ||= {}
|
33
|
+
|
34
|
+
@element_locator = locator
|
35
|
+
@capabilities = capabilities
|
36
|
+
|
37
|
+
# Timers
|
38
|
+
@find_element_timer = Maze.timers.add 'Appium - find element'
|
39
|
+
@click_element_timer = Maze.timers.add 'Appium - click element'
|
40
|
+
@clear_element_timer = Maze.timers.add 'Appium - clear element'
|
41
|
+
@send_keys_timer = Maze.timers.add 'Appium - send keys to element'
|
42
|
+
|
43
|
+
super({
|
44
|
+
'caps' => @capabilities,
|
45
|
+
'appium_lib' => {
|
46
|
+
server_url: server_url
|
47
|
+
}
|
48
|
+
}, true)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Starts the Appium driver
|
52
|
+
def start_driver
|
53
|
+
begin
|
54
|
+
$logger.info 'Starting Appium driver...'
|
55
|
+
time = Time.now
|
56
|
+
super
|
57
|
+
$logger.info "Appium driver started in #{(Time.now - time).to_i}s"
|
58
|
+
rescue => error
|
59
|
+
$logger.warn "Appium driver failed to start in #{(Time.now - time).to_i}s"
|
60
|
+
$logger.warn "#{error.class} occurred with message: #{error.message}"
|
61
|
+
raise error
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Checks for an element, waiting until it is present or the method times out
|
66
|
+
#
|
67
|
+
# @param element_id [String] the element to search for
|
68
|
+
# @param timeout [Integer] the maximum time to wait for an element to be present in seconds
|
69
|
+
# @param retry_if_stale [Boolean] enables the method to retry acquiring the element if a StaleObjectException occurs
|
70
|
+
def wait_for_element(element_id, timeout = 15, retry_if_stale = true)
|
71
|
+
wait = Selenium::WebDriver::Wait.new(timeout: timeout)
|
72
|
+
wait.until { find_element(@element_locator, element_id).displayed? }
|
73
|
+
rescue Selenium::WebDriver::Error::TimeoutError
|
74
|
+
false
|
75
|
+
rescue Selenium::WebDriver::Error::StaleElementReferenceError => e
|
76
|
+
if retry_if_stale
|
77
|
+
wait_for_element(element_id, timeout, false)
|
78
|
+
else
|
79
|
+
$logger.warn "StaleElementReferenceError occurred: #{e}"
|
80
|
+
false
|
81
|
+
end
|
82
|
+
else
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
# A wrapper around find_element adding timer functionality
|
87
|
+
def find_element_timed(element_id)
|
88
|
+
@find_element_timer.time do
|
89
|
+
find_element(@element_locator, element_id)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Clicks a given element
|
94
|
+
#
|
95
|
+
# @param element_id [String] the element to click
|
96
|
+
def click_element(element_id)
|
97
|
+
element = find_element_timed(element_id)
|
98
|
+
@click_element_timer.time do
|
99
|
+
element.click
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Clicks a given element, ignoring any NoSuchElementError
|
104
|
+
#
|
105
|
+
# @param element_id [String] the element to click
|
106
|
+
# @return [Boolean] True is the element was clicked
|
107
|
+
def click_element_if_present(element_id)
|
108
|
+
element = find_element_timed(element_id)
|
109
|
+
@click_element_timer.time do
|
110
|
+
element.click
|
111
|
+
end
|
112
|
+
true
|
113
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
# Clears a given element
|
118
|
+
#
|
119
|
+
# @param element_id [String] the element to clear
|
120
|
+
def clear_element(element_id)
|
121
|
+
element = find_element_timed(element_id)
|
122
|
+
@clear_element_timer.time do
|
123
|
+
element.clear
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Gets the application hierarchy XML
|
128
|
+
def page_source
|
129
|
+
@driver.page_source
|
130
|
+
end
|
131
|
+
|
132
|
+
# Unlocks the device
|
133
|
+
def unlock
|
134
|
+
@driver.unlock
|
135
|
+
end
|
136
|
+
|
137
|
+
# Sends keys to a given element
|
138
|
+
#
|
139
|
+
# @param element_id [String] the element to send text to
|
140
|
+
# @param text [String] the text to send
|
141
|
+
def send_keys_to_element(element_id, text)
|
142
|
+
element = find_element_timed(element_id)
|
143
|
+
@send_keys_timer.time do
|
144
|
+
element.send_keys(text)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Sets the rotation of the device
|
149
|
+
#
|
150
|
+
# @param orientation [Symbol] :portrait or :landscape
|
151
|
+
def set_rotation(orientation)
|
152
|
+
@driver.rotation = orientation
|
153
|
+
end
|
154
|
+
|
155
|
+
def window_size
|
156
|
+
@driver.window_size
|
157
|
+
end
|
158
|
+
|
159
|
+
# Send keys to the device without a specific element
|
160
|
+
#
|
161
|
+
# @param text [String] the text to send
|
162
|
+
def send_keys(text)
|
163
|
+
@driver.send_keys(text)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Sends keys to a given element, clearing it first
|
167
|
+
#
|
168
|
+
# @param element_id [String] the element to clear and send text to
|
169
|
+
# @param text [String] the text to send
|
170
|
+
def clear_and_send_keys_to_element(element_id, text)
|
171
|
+
element = find_element_timed(element_id)
|
172
|
+
@clear_element_timer.time do
|
173
|
+
element.clear
|
174
|
+
end
|
175
|
+
|
176
|
+
@send_keys_timer.time do
|
177
|
+
element.send_keys(text)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Reset the currently running application after a given timeout
|
182
|
+
#
|
183
|
+
# @param timeout [Number] the amount of time in seconds to wait before resetting
|
184
|
+
def reset_with_timeout(timeout = 0.1)
|
185
|
+
sleep(timeout)
|
186
|
+
reset
|
187
|
+
end
|
188
|
+
|
189
|
+
def device_info
|
190
|
+
driver.execute_script('mobile:deviceInfo')
|
191
|
+
end
|
192
|
+
|
193
|
+
def session_capabilities
|
194
|
+
driver.session_capabilities
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,124 @@
|
|
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 ||= {}
|
15
|
+
@capabilities = capabilities
|
16
|
+
@driver_for = driver_for
|
17
|
+
@selenium_url = selenium_url
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_element(*args)
|
21
|
+
@driver.find_element(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def navigate
|
25
|
+
@driver.navigate
|
26
|
+
end
|
27
|
+
|
28
|
+
# Refreshes the page
|
29
|
+
def refresh
|
30
|
+
@driver.refresh
|
31
|
+
end
|
32
|
+
|
33
|
+
# Quits the driver
|
34
|
+
def driver_quit
|
35
|
+
@driver.quit
|
36
|
+
end
|
37
|
+
|
38
|
+
# check if Selenium supports running javascript in the current browser
|
39
|
+
def javascript?
|
40
|
+
@driver.execute_script('return true')
|
41
|
+
rescue Selenium::WebDriver::Error::UnsupportedOperationError
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
# check if the browser supports local storage, e.g. safari 10 on browserstack
|
46
|
+
# does not have working local storage
|
47
|
+
def local_storage?
|
48
|
+
# Assume we can use local storage if we aren't able to verify by running JavaScript
|
49
|
+
return true unless javascript?
|
50
|
+
|
51
|
+
@driver.execute_script <<-JAVASCRIPT
|
52
|
+
try {
|
53
|
+
window.localStorage.setItem('__localstorage_test__', 1234)
|
54
|
+
window.localStorage.removeItem('__localstorage_test__')
|
55
|
+
|
56
|
+
return true
|
57
|
+
} catch (err) {
|
58
|
+
return false
|
59
|
+
}
|
60
|
+
JAVASCRIPT
|
61
|
+
end
|
62
|
+
|
63
|
+
# Restarts the underlying-driver in the case an unrecoverable error occurs
|
64
|
+
#
|
65
|
+
# @param attempts [Integer] The number of times we should retry a failed attempt (defaults to 6)
|
66
|
+
def restart_driver(attempts=6)
|
67
|
+
# Remove the old driver
|
68
|
+
@driver.quit
|
69
|
+
@driver = nil
|
70
|
+
|
71
|
+
start_driver(attempts)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Attempts to create a new selenium driver a given number of times
|
75
|
+
#
|
76
|
+
# @param attempts [Integer] The number of times we should retry a failed attempt (defaults to 6)
|
77
|
+
def start_driver(attempts=6)
|
78
|
+
timeout = attempts * 10
|
79
|
+
wait = Maze::Wait.new(interval: 10, timeout: timeout)
|
80
|
+
success = wait.until do
|
81
|
+
begin
|
82
|
+
create_driver(@driver_for, @selenium_url)
|
83
|
+
rescue => error
|
84
|
+
$logger.warn "#{error.class} occurred with message: #{error.message}"
|
85
|
+
end
|
86
|
+
@driver
|
87
|
+
end
|
88
|
+
|
89
|
+
unless success
|
90
|
+
$logger.error "Selenium driver failed to start after #{attempts} attempts in #{timeout} seconds"
|
91
|
+
raise RuntimeError.new("Selenium driver failed to start in #{timeout} seconds")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Creates and starts the selenium driver
|
98
|
+
def create_driver(driver_for, selenium_url=nil)
|
99
|
+
begin
|
100
|
+
$logger.info "Starting Selenium driver"
|
101
|
+
time = Time.now
|
102
|
+
if driver_for == :remote
|
103
|
+
if Maze.config.legacy_driver?
|
104
|
+
driver = ::Selenium::WebDriver.for :remote,
|
105
|
+
url: selenium_url,
|
106
|
+
desired_capabilities: @capabilities
|
107
|
+
else
|
108
|
+
driver = ::Selenium::WebDriver.for :remote,
|
109
|
+
url: selenium_url,
|
110
|
+
capabilities: @capabilities
|
111
|
+
end
|
112
|
+
else
|
113
|
+
driver = ::Selenium::WebDriver.for driver_for
|
114
|
+
end
|
115
|
+
$logger.info "Selenium driver started in #{(Time.now - time).to_i}s"
|
116
|
+
@driver = driver
|
117
|
+
rescue => error
|
118
|
+
$logger.warn "Selenium driver failed to start in #{(Time.now - time).to_i}s"
|
119
|
+
raise error
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|