bugsnag-maze-runner 9.21.0 → 9.23.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/maze-runner +3 -0
- data/lib/features/support/internal_hooks.rb +14 -2
- data/lib/maze/api/appium/app_manager.rb +91 -0
- data/lib/maze/api/appium/device_manager.rb +61 -0
- data/lib/maze/api/appium/file_manager.rb +37 -8
- data/lib/maze/api/appium/manager.rb +20 -0
- data/lib/maze/aws/sam.rb +5 -4
- data/lib/maze/client/appium/base_client.rb +14 -1
- data/lib/maze/client/appium/bs_client.rb +1 -1
- data/lib/maze/client/selenium/base_client.rb +57 -1
- data/lib/maze/client/selenium/bb_client.rb +14 -13
- data/lib/maze/client/selenium/bs_client.rb +38 -30
- data/lib/maze/configuration.rb +3 -0
- data/lib/maze/driver/appium.rb +22 -10
- data/lib/maze/driver/browser.rb +27 -0
- data/lib/maze/hooks/appium_hooks.rb +2 -2
- data/lib/maze/option/parser.rb +3 -2
- data/lib/maze/option/processor.rb +23 -24
- data/lib/maze/option/validator.rb +20 -15
- data/lib/maze.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8884abca15e25562492a3411949b061374130f0975c39da3635939c31ab94ccb
|
4
|
+
data.tar.gz: fe80b49925c6579219f0e8a307d31be1915e6cfacabe30293d3c24898e663a29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 748f7b6a945799d375b4b7d18d3183b3627f696fa229c0572e21ba5074b8d23f4c7833605b470deaaf3ef00acef2b6f54f48a3d3a4eb8f105f62de592903a20e
|
7
|
+
data.tar.gz: a06ea26f1055c4837fe9ccf22703bdcad6bab92dc58caa2c04edfa5aaefec0950c82bc39805dc5dd47fb7a8ce754dc6a6888cda11ff04f9ff9c428ec354a94a3
|
data/bin/maze-runner
CHANGED
@@ -11,6 +11,9 @@ require_relative '../lib/utils/deep_merge'
|
|
11
11
|
require_relative '../lib/maze'
|
12
12
|
|
13
13
|
require_relative '../lib/maze/appium_server'
|
14
|
+
require_relative '../lib/maze/api/appium/manager'
|
15
|
+
require_relative '../lib/maze/api/appium/app_manager'
|
16
|
+
require_relative '../lib/maze/api/appium/device_manager'
|
14
17
|
require_relative '../lib/maze/api/appium/file_manager'
|
15
18
|
require_relative '../lib/maze/api/cucumber/scenario'
|
16
19
|
require_relative '../lib/maze/api/exit_code'
|
@@ -8,7 +8,6 @@ require 'selenium-webdriver'
|
|
8
8
|
require 'uri'
|
9
9
|
|
10
10
|
BeforeAll do
|
11
|
-
|
12
11
|
Maze.check = Maze::Checks::AssertCheck.new
|
13
12
|
|
14
13
|
# Infer mode of operation from config, one of:
|
@@ -101,9 +100,16 @@ end
|
|
101
100
|
|
102
101
|
# Before each scenario
|
103
102
|
Before do |scenario|
|
103
|
+
next if scenario.status == :skipped
|
104
|
+
|
104
105
|
Maze.scenario = Maze::Api::Cucumber::Scenario.new(scenario)
|
105
106
|
|
106
|
-
#
|
107
|
+
# Skip scenario if the driver it needs has failed
|
108
|
+
if (Maze.mode == :appium || Maze.mode == :browser) && Maze.driver.failed?
|
109
|
+
skip_this_scenario
|
110
|
+
end
|
111
|
+
|
112
|
+
# Default to no dynamic retry
|
107
113
|
Maze.dynamic_retry = false
|
108
114
|
|
109
115
|
if ENV['BUILDKITE']
|
@@ -128,6 +134,8 @@ end
|
|
128
134
|
|
129
135
|
# General processing to be run after each scenario
|
130
136
|
After do |scenario|
|
137
|
+
next if scenario.status == :skipped
|
138
|
+
|
131
139
|
# If we're running on macos, take a screenshot if the scenario fails
|
132
140
|
if Maze.config.os == "macos" && scenario.status == :failed
|
133
141
|
Maze::MacosUtils.capture_screen(scenario)
|
@@ -220,6 +228,8 @@ end
|
|
220
228
|
#
|
221
229
|
# Furthermore, this hook should appear after the general hook as they are executed in reverse order by Cucumber.
|
222
230
|
After do |scenario|
|
231
|
+
next if scenario.status == :skipped
|
232
|
+
|
223
233
|
# Call any pre_complete hooks registered by the client
|
224
234
|
Maze.hooks.call_pre_complete scenario
|
225
235
|
|
@@ -234,6 +244,8 @@ end
|
|
234
244
|
# Test all requests against schemas or extra validation rules. These will only run if the schema/validation is
|
235
245
|
# specified for the specific endpoint
|
236
246
|
After do |scenario|
|
247
|
+
next if scenario.status == :skipped
|
248
|
+
|
237
249
|
['error', 'session', 'build', 'trace'].each do |endpoint|
|
238
250
|
Maze::Schemas::Validator.validate_payload_elements(Maze::Server.list_for(endpoint), endpoint)
|
239
251
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative '../../helper'
|
2
|
+
require_relative './manager'
|
3
|
+
|
4
|
+
module Maze
|
5
|
+
module Api
|
6
|
+
module Appium
|
7
|
+
# Provides operations for working with the app.
|
8
|
+
class AppManager < Maze::Api::Appium::Manager
|
9
|
+
|
10
|
+
# Activates the app
|
11
|
+
# @returns [Boolean] Whether the app was successfully launched
|
12
|
+
def activate
|
13
|
+
if failed_driver?
|
14
|
+
$logger.error 'Cannot activate the app - Appium driver failed.'
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
@driver.activate_app(@driver.app_id)
|
19
|
+
true
|
20
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
21
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
22
|
+
fail_driver
|
23
|
+
raise e
|
24
|
+
end
|
25
|
+
|
26
|
+
# Terminates the app
|
27
|
+
# @returns [Boolean] Whether the app was successfully closed
|
28
|
+
def terminate
|
29
|
+
if failed_driver?
|
30
|
+
$logger.error 'Cannot terminate the app - Appium driver failed.'
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
|
34
|
+
@driver.terminate_app(@driver.app_id)
|
35
|
+
true
|
36
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
37
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
38
|
+
fail_driver
|
39
|
+
raise e
|
40
|
+
end
|
41
|
+
|
42
|
+
# Launches the app (legacy method).
|
43
|
+
# @returns [Boolean] Whether the app was successfully launched
|
44
|
+
def launch
|
45
|
+
if failed_driver?
|
46
|
+
$logger.error 'Cannot launch the app - Appium driver failed.'
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
@driver.launch_app
|
51
|
+
true
|
52
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
53
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
54
|
+
fail_driver
|
55
|
+
raise e
|
56
|
+
end
|
57
|
+
|
58
|
+
# Closes the app (legacy method).
|
59
|
+
# @returns [Boolean] Whether the app was successfully closed
|
60
|
+
def close
|
61
|
+
if failed_driver?
|
62
|
+
$logger.error 'Cannot close the app - Appium driver failed.'
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
|
66
|
+
@driver.close_app
|
67
|
+
true
|
68
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
69
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
70
|
+
fail_driver
|
71
|
+
raise e
|
72
|
+
end
|
73
|
+
|
74
|
+
# Gets the app state.
|
75
|
+
# @returns [Symbol, nil] The app state, such as :not_running, :running_in_foreground, :running_in_background - of nil if the driver has failed.
|
76
|
+
def state
|
77
|
+
if failed_driver?
|
78
|
+
$logger.error('Cannot get the app state - Appium driver failed.')
|
79
|
+
return nil
|
80
|
+
end
|
81
|
+
|
82
|
+
@driver.app_state(@driver.app_id)
|
83
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
84
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
85
|
+
fail_driver
|
86
|
+
raise e
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative '../../helper'
|
3
|
+
require_relative './manager'
|
4
|
+
|
5
|
+
module Maze
|
6
|
+
module Api
|
7
|
+
module Appium
|
8
|
+
# Provides operations for working with the app.
|
9
|
+
class DeviceManager < Maze::Api::Appium::Manager
|
10
|
+
|
11
|
+
# Unlocks the device.
|
12
|
+
# @returns [Boolean] Success status
|
13
|
+
def unlock
|
14
|
+
if failed_driver?
|
15
|
+
$logger.error 'Cannot unlock the device - Appium driver failed.'
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
|
19
|
+
@driver.unlock
|
20
|
+
true
|
21
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
22
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
23
|
+
fail_driver
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets the rotation of the device.
|
28
|
+
# @param orientation [Symbol] The orientation to set the device to, :portrait or :landscape
|
29
|
+
# @returns [Boolean] Success status
|
30
|
+
def set_rotation(orientation)
|
31
|
+
if failed_driver?
|
32
|
+
$logger.error 'Cannot set the device rotation - Appium driver failed.'
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
|
36
|
+
@driver.set_rotation(orientation)
|
37
|
+
true
|
38
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
39
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
40
|
+
fail_driver
|
41
|
+
raise e
|
42
|
+
end
|
43
|
+
|
44
|
+
# Gets the device info, in JSON format
|
45
|
+
# @returns [String, nil] Device info or nil
|
46
|
+
def info
|
47
|
+
if failed_driver?
|
48
|
+
$logger.error 'Cannot get the device info - Appium driver failed.'
|
49
|
+
return nil
|
50
|
+
end
|
51
|
+
|
52
|
+
JSON.generate(@driver.device_info)
|
53
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
54
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
55
|
+
fail_driver
|
56
|
+
raise e
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -1,29 +1,43 @@
|
|
1
|
+
require_relative '../../helper'
|
2
|
+
require_relative './manager'
|
3
|
+
|
1
4
|
module Maze
|
2
5
|
module Api
|
3
6
|
module Appium
|
4
7
|
# Provides operations for working with files during Appium runs.
|
5
|
-
class FileManager
|
6
|
-
# param driver
|
7
|
-
def initialize
|
8
|
-
@driver = Maze.driver
|
9
|
-
end
|
10
|
-
|
8
|
+
class FileManager < Maze::Api::Appium::Manager
|
11
9
|
# Creates a file with the given contents on the device (using Appium). The file will be located in the app's
|
12
10
|
# documents directory for iOS. On Android, it will be /sdcard/Android/data/<app-id>/files unless
|
13
11
|
# Maze.config.android_app_files_directory has been set.
|
14
12
|
# @param contents [String] Content of the file to be written
|
15
13
|
# @param filename [String] Name (with no path) of the file to be written on the device
|
14
|
+
# @return [Boolean] Whether the file was successfully written to the device
|
16
15
|
def write_app_file(contents, filename)
|
16
|
+
if failed_driver?
|
17
|
+
$logger.error 'Cannot write file to device - Appium driver failed.'
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
17
21
|
path = case Maze::Helper.get_current_platform
|
18
22
|
when 'ios'
|
19
23
|
"@#{@driver.app_id}/Documents/#{filename}"
|
20
24
|
when 'android'
|
21
25
|
directory = Maze.config.android_app_files_directory || "/sdcard/Android/data/#{@driver.app_id}/files"
|
22
26
|
"#{directory}/#{filename}"
|
27
|
+
else
|
28
|
+
raise 'write_app_file is not supported on this platform'
|
23
29
|
end
|
24
30
|
|
25
31
|
$logger.trace "Pushing file to '#{path}' with contents: #{contents}"
|
26
32
|
@driver.push_file(path, contents)
|
33
|
+
true
|
34
|
+
rescue Selenium::WebDriver::Error::UnknownError => e
|
35
|
+
$logger.error "Error writing file to device: #{e.message}"
|
36
|
+
false
|
37
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
38
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
39
|
+
fail_driver
|
40
|
+
raise e
|
27
41
|
end
|
28
42
|
|
29
43
|
# Attempts to retrieve a given file from the device (using Appium). The default location for the file will be
|
@@ -31,9 +45,15 @@ module Maze
|
|
31
45
|
# Maze.config.android_app_files_directory has been set.
|
32
46
|
# @param filename [String] Name (with no path) of the file to be retrieved from the device
|
33
47
|
# @param directory [String] Directory on the device where the file is located (optional)
|
48
|
+
# @return [String, nil] The content of the file read, or nil
|
34
49
|
def read_app_file(filename, directory = nil)
|
50
|
+
if failed_driver?
|
51
|
+
$logger.error 'Cannot read file from device - Appium driver failed.'
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
|
35
55
|
if directory
|
36
|
-
path = directory
|
56
|
+
path = "#{directory}/#{filename}"
|
37
57
|
else
|
38
58
|
path = case Maze::Helper.get_current_platform
|
39
59
|
when 'ios'
|
@@ -41,11 +61,20 @@ module Maze
|
|
41
61
|
when 'android'
|
42
62
|
dir = Maze.config.android_app_files_directory || "/sdcard/Android/data/#{@driver.app_id}/files"
|
43
63
|
"#{dir}/#{filename}"
|
64
|
+
else
|
65
|
+
raise 'read_app_file is not supported on this platform'
|
44
66
|
end
|
45
67
|
end
|
46
68
|
|
47
69
|
$logger.trace "Attempting to read file from '#{path}'"
|
48
|
-
|
70
|
+
@driver.pull_file(path)
|
71
|
+
rescue Selenium::WebDriver::Error::UnknownError => e
|
72
|
+
$logger.error "Error reading file from device: #{e.message}"
|
73
|
+
nil
|
74
|
+
rescue Selenium::WebDriver::Error::ServerError => e
|
75
|
+
# Assume the remote appium session has stopped, so crash out of the session
|
76
|
+
fail_driver
|
77
|
+
raise e
|
49
78
|
end
|
50
79
|
end
|
51
80
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Maze
|
2
|
+
module Api
|
3
|
+
module Appium
|
4
|
+
# Base class for all Appium managers.
|
5
|
+
class Manager
|
6
|
+
def initialize
|
7
|
+
@driver = Maze.driver
|
8
|
+
end
|
9
|
+
|
10
|
+
def failed_driver?
|
11
|
+
@driver.failed?
|
12
|
+
end
|
13
|
+
|
14
|
+
def fail_driver
|
15
|
+
@driver.fail_driver
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/maze/aws/sam.rb
CHANGED
@@ -65,7 +65,7 @@ module Maze
|
|
65
65
|
unless valid?(output)
|
66
66
|
message = <<-WARNING
|
67
67
|
The lambda function did not successfully complete.
|
68
|
-
This may be expected and a normal result of the
|
68
|
+
This may be expected and a normal result of the test execution.
|
69
69
|
The listed cause is:
|
70
70
|
> #{output.last.chomp}
|
71
71
|
|
@@ -75,11 +75,12 @@ module Maze
|
|
75
75
|
$logger.warn message
|
76
76
|
end
|
77
77
|
|
78
|
-
# Attempt to parse
|
79
|
-
#
|
78
|
+
# Attempt to parse response line of the output.
|
79
|
+
# It's possible for a Lambda to output nothing,
|
80
80
|
# e.g. if it forcefully exited, so we allow JSON parse failures here
|
81
81
|
begin
|
82
|
-
|
82
|
+
response_line = output.find { |line| /{.*}/.match(line.strip) }
|
83
|
+
parsed_output = JSON.parse(response_line)
|
83
84
|
rescue JSON::ParserError
|
84
85
|
return {}
|
85
86
|
end
|
@@ -33,6 +33,9 @@ module Maze
|
|
33
33
|
$logger.debug "session_capabilities: #{Maze.driver.session_capabilities.inspect}"
|
34
34
|
end
|
35
35
|
|
36
|
+
# Log the device information after it's started
|
37
|
+
write_device_info
|
38
|
+
|
36
39
|
# Ensure the device is unlocked
|
37
40
|
begin
|
38
41
|
Maze.driver.unlock
|
@@ -56,6 +59,16 @@ module Maze
|
|
56
59
|
raise 'Method not implemented by this class'
|
57
60
|
end
|
58
61
|
|
62
|
+
def write_device_info
|
63
|
+
info = Maze.driver.device_info
|
64
|
+
filepath = File.join(Dir.pwd, 'maze_output', 'device_info.json')
|
65
|
+
File.open(filepath, 'w+') do |file|
|
66
|
+
file.puts(JSON.pretty_generate(info))
|
67
|
+
end
|
68
|
+
rescue => error
|
69
|
+
$logger.warn "Could not write device information file, #{error.message}"
|
70
|
+
end
|
71
|
+
|
59
72
|
def attempt_start_driver(config)
|
60
73
|
config.capabilities = device_capabilities
|
61
74
|
driver = Maze::Driver::Appium.new config.appium_server_url,
|
@@ -134,7 +147,7 @@ module Maze
|
|
134
147
|
end
|
135
148
|
|
136
149
|
def stop_session
|
137
|
-
Maze.driver
|
150
|
+
Maze.driver.driver_quit unless Maze.driver.failed?
|
138
151
|
Maze::AppiumServer.stop if Maze::AppiumServer.running
|
139
152
|
end
|
140
153
|
end
|
@@ -67,7 +67,7 @@ module Maze
|
|
67
67
|
|
68
68
|
def log_run_intro
|
69
69
|
# Log a link to the BrowserStack session search dashboard
|
70
|
-
url = "https://app-automate.browserstack.com/
|
70
|
+
url = "https://app-automate.browserstack.com/projects/#{project_name_capabilities[:project]}/builds/#{Maze.run_uuid}/1?tab=tests"
|
71
71
|
$logger.info Maze::Loggers::LogUtil.linkify(url, 'BrowserStack session(s)')
|
72
72
|
end
|
73
73
|
|
@@ -6,12 +6,68 @@ module Maze
|
|
6
6
|
raise 'Method not implemented by this class'
|
7
7
|
end
|
8
8
|
|
9
|
+
def start_driver(config, selenium_url, max_attempts = 5)
|
10
|
+
attempts = 0
|
11
|
+
|
12
|
+
while attempts < max_attempts && Maze.driver.nil?
|
13
|
+
attempts += 1
|
14
|
+
start_error = nil
|
15
|
+
|
16
|
+
$logger.trace "Attempting to start Selenium driver with capabilities: #{config.capabilities.to_json}"
|
17
|
+
$logger.trace "Attempt #{attempts}"
|
18
|
+
begin
|
19
|
+
Maze.driver = Maze::Driver::Browser.new(:remote, selenium_url, config.capabilities)
|
20
|
+
Maze.driver.start_driver
|
21
|
+
rescue => error
|
22
|
+
Maze.driver = nil
|
23
|
+
$logger.error "Session creation failed: #{error}"
|
24
|
+
start_error = error
|
25
|
+
end
|
26
|
+
|
27
|
+
unless Maze.driver
|
28
|
+
interval = handle_start_error(config, start_error)
|
29
|
+
if interval.nil? || attempts >= max_attempts
|
30
|
+
$logger.error 'Failed to create Selenium driver, exiting'
|
31
|
+
Kernel.exit(::Maze::Api::ExitCode::SESSION_CREATION_FAILURE)
|
32
|
+
else
|
33
|
+
$logger.warn "Failed to create Selenium driver, retrying in #{interval} seconds"
|
34
|
+
$logger.info "Error: #{start_error.message}" if start_error
|
35
|
+
Kernel.sleep(interval)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def handle_start_error(config, error)
|
42
|
+
notify = true
|
43
|
+
interval = nil
|
44
|
+
|
45
|
+
# Used if we have a want to determine fatal errors later
|
46
|
+
case error.class.to_s
|
47
|
+
when 'Selenium::WebDriver::Error::WebDriverError'
|
48
|
+
interval = 5
|
49
|
+
notify = false
|
50
|
+
else
|
51
|
+
interval = 10
|
52
|
+
end
|
53
|
+
|
54
|
+
Bugsnag.notify error if notify
|
55
|
+
|
56
|
+
unless config.browser_list.empty?
|
57
|
+
# If the list is empty we have only one browser to continue with
|
58
|
+
config.browser = config.browser_list.shift
|
59
|
+
config.capabilities = create_capabilities(config)
|
60
|
+
end
|
61
|
+
|
62
|
+
interval
|
63
|
+
end
|
64
|
+
|
9
65
|
def log_run_outro
|
10
66
|
raise 'Method not implemented by this class'
|
11
67
|
end
|
12
68
|
|
13
69
|
def stop_session
|
14
|
-
Maze.driver
|
70
|
+
Maze.driver.driver_quit unless Maze.driver.failed?
|
15
71
|
end
|
16
72
|
end
|
17
73
|
end
|
@@ -4,6 +4,19 @@ module Maze
|
|
4
4
|
class BitBarClient < BaseClient
|
5
5
|
def start_session
|
6
6
|
config = Maze.config
|
7
|
+
if Maze::Client::BitBarClientUtils.use_local_tunnel?
|
8
|
+
Maze::Client::BitBarClientUtils.start_local_tunnel config.sb_local,
|
9
|
+
config.username,
|
10
|
+
config.access_key
|
11
|
+
end
|
12
|
+
|
13
|
+
create_capabilities(config)
|
14
|
+
|
15
|
+
selenium_url = Maze.config.selenium_server_url
|
16
|
+
start_driver(config, selenium_url)
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_capabilities(config)
|
7
20
|
capabilities = ::Selenium::WebDriver::Remote::Capabilities.new
|
8
21
|
capabilities['bitbar_apiKey'] = config.access_key
|
9
22
|
browsers = YAML.safe_load(File.read("#{__dir__}/bb_browsers.yml"))
|
@@ -13,20 +26,8 @@ module Maze
|
|
13
26
|
capabilities.merge! JSON.parse(config.capabilities_option)
|
14
27
|
capabilities['bitbar:options']['testTimeout'] = 900
|
15
28
|
capabilities['acceptInsecureCerts'] = true unless Maze.config.browser.include? 'ie_'
|
29
|
+
capabilities['bitbar_apiKey'] = config.access_key if Maze::Client::BitBarClientUtils.use_local_tunnel?
|
16
30
|
config.capabilities = capabilities
|
17
|
-
|
18
|
-
if Maze::Client::BitBarClientUtils.use_local_tunnel?
|
19
|
-
capabilities['bitbar_apiKey'] = config.access_key
|
20
|
-
Maze::Client::BitBarClientUtils.start_local_tunnel config.sb_local,
|
21
|
-
config.username,
|
22
|
-
config.access_key
|
23
|
-
end
|
24
|
-
|
25
|
-
selenium_url = Maze.config.selenium_server_url
|
26
|
-
|
27
|
-
$logger.trace "Starting Selenium driver with capabilities: #{config.capabilities.to_json}"
|
28
|
-
Maze.driver = Maze::Driver::Browser.new :remote, selenium_url, config.capabilities
|
29
|
-
Maze.driver.start_driver
|
30
31
|
end
|
31
32
|
|
32
33
|
def log_run_outro
|
@@ -5,55 +5,63 @@ module Maze
|
|
5
5
|
module Selenium
|
6
6
|
class BrowserStackClient < BaseClient
|
7
7
|
def start_session
|
8
|
-
# Set up the capabilities
|
9
|
-
browsers = YAML.safe_load(File.read("#{__dir__}/bs_browsers.yml"))
|
10
|
-
|
11
8
|
config = Maze.config
|
12
|
-
browser = browsers[config.browser]
|
13
9
|
|
10
|
+
# Start the tunnel
|
11
|
+
Maze::Client::BrowserStackClientUtils.start_local_tunnel config.bs_local,
|
12
|
+
Maze.run_uuid,
|
13
|
+
config.access_key
|
14
|
+
|
15
|
+
create_capabilities(config)
|
16
|
+
|
17
|
+
# Start the driver
|
18
|
+
selenium_url = "https://#{config.username}:#{config.access_key}@hub.browserstack.com/wd/hub"
|
19
|
+
start_driver(config, selenium_url)
|
20
|
+
|
21
|
+
# Log details for the session
|
22
|
+
log_session_info
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_capabilities(config)
|
14
26
|
if config.legacy_driver?
|
15
27
|
capabilities = ::Selenium::WebDriver::Remote::Capabilities.new
|
16
28
|
capabilities['browserstack.local'] = 'true'
|
17
29
|
capabilities['browserstack.localIdentifier'] = Maze.run_uuid
|
18
30
|
capabilities['browserstack.console'] = 'errors'
|
19
31
|
capabilities['acceptInsecureCerts'] = 'true'
|
20
|
-
|
21
|
-
# Convert W3S capabilities to JSON-WP
|
22
|
-
capabilities['browser'] = browser['browserName']
|
23
|
-
capabilities['browser_version'] = browser['browserVersion']
|
24
|
-
capabilities['device'] = browser['device']
|
25
|
-
capabilities['os'] = browser['os']
|
26
|
-
capabilities['os_version'] = browser['osVersion']
|
27
|
-
|
28
32
|
capabilities.merge! JSON.parse(config.capabilities_option)
|
29
33
|
capabilities.merge! project_name_capabilities
|
30
|
-
config
|
34
|
+
add_browser_capabilities(config, capabilities)
|
31
35
|
else
|
32
|
-
|
36
|
+
raw_capabilities = {
|
33
37
|
'acceptInsecureCerts' => true,
|
34
38
|
'bstack:options' => {
|
35
39
|
'local' => 'true',
|
36
40
|
'localIdentifier' => Maze.run_uuid
|
37
41
|
}
|
38
42
|
}
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
raw_capabilities.deep_merge! JSON.parse(config.capabilities_option)
|
44
|
+
raw_capabilities.merge! project_name_capabilities
|
45
|
+
add_browser_capabilities(config, raw_capabilities)
|
46
|
+
capabilities = ::Selenium::WebDriver::Remote::Capabilities.new raw_capabilities
|
43
47
|
end
|
48
|
+
config.capabilities = capabilities
|
49
|
+
end
|
44
50
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
config.access_key
|
49
|
-
|
50
|
-
# Start the driver
|
51
|
-
selenium_url = "https://#{config.username}:#{config.access_key}@hub.browserstack.com/wd/hub"
|
52
|
-
Maze.driver = Maze::Driver::Browser.new :remote, selenium_url, config.capabilities
|
53
|
-
Maze.driver.start_driver
|
51
|
+
def add_browser_capabilities(config, capabilities)
|
52
|
+
browsers = YAML.safe_load(File.read("#{__dir__}/bs_browsers.yml"))
|
53
|
+
browser = browsers[config.browser]
|
54
54
|
|
55
|
-
|
56
|
-
|
55
|
+
if config.legacy_driver?
|
56
|
+
# Convert W3S capabilities to JSON-WP
|
57
|
+
capabilities['browser'] = browser['browserName']
|
58
|
+
capabilities['browser_version'] = browser['browserVersion']
|
59
|
+
capabilities['device'] = browser['device']
|
60
|
+
capabilities['os'] = browser['os']
|
61
|
+
capabilities['os_version'] = browser['osVersion']
|
62
|
+
else
|
63
|
+
capabilities.deep_merge! browser
|
64
|
+
end
|
57
65
|
end
|
58
66
|
|
59
67
|
def stop_session
|
@@ -86,7 +94,7 @@ module Maze
|
|
86
94
|
|
87
95
|
def log_session_info
|
88
96
|
# Log a link to the BrowserStack session search dashboard
|
89
|
-
url = "https://automate.browserstack.com/
|
97
|
+
url = "https://automate.browserstack.com/projects/#{project_name_capabilities[:project]}/builds/#{Maze.run_uuid}/1?tab=tests"
|
90
98
|
$logger.info Maze::Loggers::LogUtil.linkify url, 'BrowserStack session(s)'
|
91
99
|
end
|
92
100
|
end
|
data/lib/maze/configuration.rb
CHANGED
data/lib/maze/driver/appium.rb
CHANGED
@@ -20,7 +20,7 @@ module Maze
|
|
20
20
|
attr_reader :device_type
|
21
21
|
|
22
22
|
# @!attribute [r] capabilities
|
23
|
-
# @return [Hash] The capabilities used to launch the
|
23
|
+
# @return [Hash] The capabilities used to launch the Appium session
|
24
24
|
attr_reader :capabilities
|
25
25
|
|
26
26
|
# Creates the Appium driver
|
@@ -32,6 +32,7 @@ module Maze
|
|
32
32
|
# Sets up identifiers for ease of connecting jobs
|
33
33
|
capabilities ||= {}
|
34
34
|
|
35
|
+
@failed = false
|
35
36
|
@element_locator = locator
|
36
37
|
@capabilities = capabilities
|
37
38
|
|
@@ -64,6 +65,17 @@ module Maze
|
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
68
|
+
# Whether the driver has known to have failed (it may still have failed and us not know yet)
|
69
|
+
def failed?
|
70
|
+
@failed
|
71
|
+
end
|
72
|
+
|
73
|
+
# Marks the driver as failed
|
74
|
+
def fail_driver
|
75
|
+
$logger.error 'Appium driver failed, remaining scenarios will be skipped'
|
76
|
+
@failed = true
|
77
|
+
end
|
78
|
+
|
67
79
|
# Checks for an element, waiting until it is present or the method times out
|
68
80
|
#
|
69
81
|
# @param element_id [String] the element to search for
|
@@ -83,7 +95,7 @@ module Maze
|
|
83
95
|
end
|
84
96
|
rescue Selenium::WebDriver::Error::ServerError => e
|
85
97
|
# Assume the remote appium session has stopped, so crash out of the session
|
86
|
-
|
98
|
+
fail_driver
|
87
99
|
raise e
|
88
100
|
else
|
89
101
|
true
|
@@ -94,7 +106,7 @@ module Maze
|
|
94
106
|
super
|
95
107
|
rescue Selenium::WebDriver::Error::ServerError => e
|
96
108
|
# Assume the remote appium session has stopped, so crash out of the session
|
97
|
-
|
109
|
+
fail_driver
|
98
110
|
raise e
|
99
111
|
end
|
100
112
|
|
@@ -103,7 +115,7 @@ module Maze
|
|
103
115
|
super
|
104
116
|
rescue Selenium::WebDriver::Error::ServerError => e
|
105
117
|
# Assume the remote appium session has stopped, so crash out of the session
|
106
|
-
|
118
|
+
fail_driver
|
107
119
|
raise e
|
108
120
|
end
|
109
121
|
|
@@ -114,7 +126,7 @@ module Maze
|
|
114
126
|
end
|
115
127
|
rescue Selenium::WebDriver::Error::ServerError => e
|
116
128
|
# Assume the remote appium session has stopped, so crash out of the session
|
117
|
-
|
129
|
+
fail_driver
|
118
130
|
raise e
|
119
131
|
end
|
120
132
|
|
@@ -128,7 +140,7 @@ module Maze
|
|
128
140
|
end
|
129
141
|
rescue Selenium::WebDriver::Error::ServerError => e
|
130
142
|
# Assume the remote appium session has stopped, so crash out of the session
|
131
|
-
|
143
|
+
fail_driver
|
132
144
|
raise e
|
133
145
|
end
|
134
146
|
|
@@ -146,7 +158,7 @@ module Maze
|
|
146
158
|
false
|
147
159
|
rescue Selenium::WebDriver::Error::ServerError => e
|
148
160
|
# Assume the remote appium session has stopped, so crash out of the session
|
149
|
-
|
161
|
+
fail_driver
|
150
162
|
raise e
|
151
163
|
end
|
152
164
|
|
@@ -160,7 +172,7 @@ module Maze
|
|
160
172
|
end
|
161
173
|
rescue Selenium::WebDriver::Error::ServerError => e
|
162
174
|
# Assume the remote appium session has stopped, so crash out of the session
|
163
|
-
|
175
|
+
fail_driver
|
164
176
|
raise e
|
165
177
|
end
|
166
178
|
|
@@ -185,7 +197,7 @@ module Maze
|
|
185
197
|
end
|
186
198
|
rescue Selenium::WebDriver::Error::ServerError => e
|
187
199
|
# Assume the remote appium session has stopped, so crash out of the session
|
188
|
-
|
200
|
+
fail_driver
|
189
201
|
raise e
|
190
202
|
end
|
191
203
|
|
@@ -222,7 +234,7 @@ module Maze
|
|
222
234
|
end
|
223
235
|
rescue Selenium::WebDriver::Error::ServerError => e
|
224
236
|
# Assume the remote appium session has stopped, so crash out of the session
|
225
|
-
|
237
|
+
fail_driver
|
226
238
|
raise e
|
227
239
|
end
|
228
240
|
|
data/lib/maze/driver/browser.rb
CHANGED
@@ -13,15 +13,41 @@ module Maze
|
|
13
13
|
|
14
14
|
def initialize(driver_for, selenium_url=nil, capabilities=nil)
|
15
15
|
capabilities ||= {}
|
16
|
+
@failed = false
|
16
17
|
@capabilities = capabilities
|
17
18
|
@driver_for = driver_for
|
18
19
|
@selenium_url = selenium_url
|
19
20
|
end
|
20
21
|
|
22
|
+
# Whether the driver has known to have failed (it may still have failed and us not know yet)
|
23
|
+
def failed?
|
24
|
+
@failed
|
25
|
+
end
|
26
|
+
|
27
|
+
# Marks the driver as failed
|
28
|
+
def fail_driver
|
29
|
+
$logger.error 'Selenium driver failed, remaining scenarios will be skipped'
|
30
|
+
@failed = true
|
31
|
+
end
|
32
|
+
|
21
33
|
def find_element(*args)
|
22
34
|
@driver.find_element(*args)
|
23
35
|
end
|
24
36
|
|
37
|
+
def wait_for_element(id)
|
38
|
+
@driver.find_element(id: id)
|
39
|
+
end
|
40
|
+
|
41
|
+
def click_element(id)
|
42
|
+
element = @driver.find_element(id: id)
|
43
|
+
|
44
|
+
if $browser.mobile?
|
45
|
+
element.click
|
46
|
+
else
|
47
|
+
@driver.action.move_to(element).click.perform
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
25
51
|
def navigate
|
26
52
|
@driver.navigate
|
27
53
|
end
|
@@ -128,6 +154,7 @@ module Maze
|
|
128
154
|
end
|
129
155
|
$logger.info "Selenium driver started in #{(Time.now - time).to_i}s"
|
130
156
|
@driver = driver
|
157
|
+
@failed = false
|
131
158
|
rescue => error
|
132
159
|
Bugsnag.notify error
|
133
160
|
$logger.warn "Selenium driver failed to start in #{(Time.now - time).to_i}s"
|
@@ -42,11 +42,11 @@ module Maze
|
|
42
42
|
# Reset the server to ensure that test fixtures cannot fetch
|
43
43
|
# commands from the previous scenario (in idempotent mode).
|
44
44
|
begin
|
45
|
-
Maze.driver.terminate_app Maze.driver
|
45
|
+
Maze.driver.terminate_app Maze.driver.app_id
|
46
46
|
rescue Selenium::WebDriver::Error::UnknownError, Selenium::WebDriver::Error::InvalidSessionIdError
|
47
47
|
if Maze.config.appium_version && Maze.config.appium_version.to_f < 2.0
|
48
48
|
$logger.warn 'terminate_app failed, using the slower but more forceful close_app instead'
|
49
|
-
Maze.driver
|
49
|
+
Maze.driver.close_app
|
50
50
|
else
|
51
51
|
$logger.warn 'terminate_app failed, future errors may occur if the application did not close remotely'
|
52
52
|
end
|
data/lib/maze/option/parser.rb
CHANGED
@@ -124,9 +124,10 @@ module Maze
|
|
124
124
|
type: :string,
|
125
125
|
multi: true
|
126
126
|
opt Option::BROWSER,
|
127
|
-
'Browser to use (an entry in <farm>_browsers.yml)',
|
127
|
+
'Browser to use (an entry in <farm>_browsers.yml). Can be listed multiple times to have a prioritised list of browsers',
|
128
128
|
short: :none,
|
129
|
-
type: :string
|
129
|
+
type: :string,
|
130
|
+
multi: true
|
130
131
|
opt Option::BROWSER_VERSION,
|
131
132
|
'Browser version to use (applies to entries in <farm>_browsers.yml that do not include a version)',
|
132
133
|
short: :none,
|
@@ -57,21 +57,18 @@ module Maze
|
|
57
57
|
case config.farm
|
58
58
|
when :bs
|
59
59
|
device_option = options[Maze::Option::DEVICE]
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
config.device = device_option.first
|
65
|
-
config.device_list = device_option.drop(1)
|
66
|
-
else
|
67
|
-
config.device = device_option
|
68
|
-
config.device_list = []
|
69
|
-
end
|
60
|
+
browser_option = options[Maze::Option::BROWSER]
|
61
|
+
if !device_option.empty?
|
62
|
+
config.device = device_option.first
|
63
|
+
config.device_list = device_option.drop(1)
|
70
64
|
if config.legacy_driver?
|
71
65
|
config.os_version = Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH[config.device]['os_version'].to_f
|
72
66
|
else
|
73
67
|
config.os_version = Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH[config.device]['platformVersion'].to_f
|
74
68
|
end
|
69
|
+
elsif !browser_option.empty?
|
70
|
+
config.browser = browser_option.first
|
71
|
+
config.browser_list = browser_option.drop(1)
|
75
72
|
end
|
76
73
|
config.bs_local = Maze::Helper.expand_path(options[Maze::Option::BS_LOCAL])
|
77
74
|
config.appium_version = options[Maze::Option::APPIUM_VERSION]
|
@@ -83,18 +80,20 @@ module Maze
|
|
83
80
|
config.access_key = options[Maze::Option::ACCESS_KEY]
|
84
81
|
config.appium_version = options[Maze::Option::APPIUM_VERSION]
|
85
82
|
device_option = options[Maze::Option::DEVICE]
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
config.
|
90
|
-
|
91
|
-
|
92
|
-
if
|
93
|
-
config.
|
94
|
-
config.
|
83
|
+
browser_option = options[Maze::Option::BROWSER]
|
84
|
+
browser_version = options[Maze::Option::BROWSER_VERSION]
|
85
|
+
if !device_option.empty?
|
86
|
+
config.device = device_option.first
|
87
|
+
config.device_list = device_option.drop(1)
|
88
|
+
elsif !browser_option.empty?
|
89
|
+
if browser_version.nil?
|
90
|
+
config.browser = browser_option.first
|
91
|
+
config.browser_list = browser_option.drop(1)
|
95
92
|
else
|
96
|
-
|
97
|
-
config.
|
93
|
+
# Dropping all but the first browser as the version is specified
|
94
|
+
config.browser = browser_option.first
|
95
|
+
config.browser_list = []
|
96
|
+
config.browser_version = browser_version
|
98
97
|
end
|
99
98
|
end
|
100
99
|
config.os = options[Maze::Option::OS]
|
@@ -104,9 +103,7 @@ module Maze
|
|
104
103
|
config.selenium_server_url = options[Maze::Option::SELENIUM_SERVER]
|
105
104
|
config.app_bundle_id = options[Maze::Option::APP_BUNDLE_ID]
|
106
105
|
when :local then
|
107
|
-
if options[Maze::Option::BROWSER]
|
108
|
-
config.browser = options[Maze::Option::BROWSER]
|
109
|
-
else
|
106
|
+
if options[Maze::Option::BROWSER].empty?
|
110
107
|
os = config.os = options[Maze::Option::OS].downcase
|
111
108
|
config.os_version = options[Maze::Option::OS_VERSION].to_f unless options[Maze::Option::OS_VERSION].nil?
|
112
109
|
config.appium_server_url = options[Maze::Option::APPIUM_SERVER]
|
@@ -116,6 +113,8 @@ module Maze
|
|
116
113
|
config.apple_team_id = options[Maze::Option::APPLE_TEAM_ID]
|
117
114
|
config.device_id = options[Maze::Option::UDID]
|
118
115
|
end
|
116
|
+
else
|
117
|
+
config.browser = options[Maze::Option::BROWSER].first
|
119
118
|
end
|
120
119
|
when :none
|
121
120
|
if options[Maze::Option::OS]
|
@@ -53,17 +53,17 @@ module Maze
|
|
53
53
|
# Device
|
54
54
|
browser = options[Option::BROWSER]
|
55
55
|
device = options[Option::DEVICE]
|
56
|
-
if browser.
|
56
|
+
if browser.empty? && device.empty?
|
57
57
|
errors << "Either --#{Option::BROWSER} or --#{Option::DEVICE} must be specified"
|
58
|
-
elsif browser
|
59
|
-
|
58
|
+
elsif !browser.empty?
|
60
59
|
browsers = YAML.safe_load(File.read("#{__dir__}/../client/selenium/bs_browsers.yml"))
|
61
60
|
|
62
|
-
|
61
|
+
rejected_browsers = browser.reject { |br| browsers.include? br }
|
62
|
+
unless rejected_browsers.empty?
|
63
63
|
browser_list = browsers.keys.join ', '
|
64
|
-
errors << "Browser
|
64
|
+
errors << "Browser types '#{rejected_browsers.join(', ')}' unknown on BrowserStack. Must be one of: #{browser_list}."
|
65
65
|
end
|
66
|
-
elsif device
|
66
|
+
elsif !device.empty?
|
67
67
|
device.each do |device_key|
|
68
68
|
next if Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH.key? device_key
|
69
69
|
errors << "Device type '#{device_key}' unknown on BrowserStack. Must be one of #{Maze::Client::Appium::BrowserStackDevices::DEVICE_HASH.keys}"
|
@@ -89,25 +89,30 @@ module Maze
|
|
89
89
|
def validate_bitbar(options, errors)
|
90
90
|
browser = options[Option::BROWSER]
|
91
91
|
device = options[Option::DEVICE]
|
92
|
-
|
92
|
+
|
93
93
|
errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
|
94
94
|
errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
|
95
95
|
|
96
96
|
# Device
|
97
|
-
if browser.
|
97
|
+
if browser.empty? && device.empty?
|
98
98
|
errors << "Either --#{Option::BROWSER} or --#{Option::DEVICE} must be specified"
|
99
|
-
elsif browser
|
99
|
+
elsif !browser.empty?
|
100
100
|
browsers = YAML.safe_load(File.read("#{__dir__}/../client/selenium/bb_browsers.yml"))
|
101
101
|
|
102
|
-
|
103
|
-
|
104
|
-
|
102
|
+
rejected_browsers = browser.reject { |br| browsers.include? br }
|
103
|
+
if rejected_browsers.empty?
|
104
|
+
if options[Option::BROWSER_VERSION].nil?
|
105
|
+
browser.each do |br|
|
106
|
+
next if browsers[br].include?('version')
|
107
|
+
errors << "--#{Option::BROWSER_VERSION} must be specified for browser '#{br}'"
|
108
|
+
end
|
105
109
|
end
|
106
110
|
else
|
107
111
|
browser_list = browsers.keys.join ', '
|
108
|
-
errors << "Browser
|
112
|
+
errors << "Browser types '#{rejected_browsers.join(', ')}' unknown on BitBar. Must be one of: #{browser_list}."
|
109
113
|
end
|
110
|
-
|
114
|
+
|
115
|
+
elsif !device.empty?
|
111
116
|
app = Maze::Helper.read_at_arg_file options[Option::APP]
|
112
117
|
if app.nil?
|
113
118
|
errors << "--#{Option::APP} must be provided when running on a device"
|
@@ -123,7 +128,7 @@ module Maze
|
|
123
128
|
|
124
129
|
# Validates Local device options
|
125
130
|
def validate_local(options, errors)
|
126
|
-
if options[Option::BROWSER].
|
131
|
+
if options[Option::BROWSER].empty?
|
127
132
|
errors << "--#{Option::APP} must be specified" if options[Option::APP].nil?
|
128
133
|
|
129
134
|
# OS
|
data/lib/maze.rb
CHANGED
@@ -8,7 +8,7 @@ require_relative 'maze/timers'
|
|
8
8
|
# providing an alternative to the proliferation of global variables or singletons.
|
9
9
|
module Maze
|
10
10
|
|
11
|
-
VERSION = '9.
|
11
|
+
VERSION = '9.23.0'
|
12
12
|
|
13
13
|
class << self
|
14
14
|
attr_accessor :check, :driver, :internal_hooks, :mode, :start_time, :dynamic_retry, :public_address,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bugsnag-maze-runner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 9.
|
4
|
+
version: 9.23.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steve Kirkland
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-02-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cucumber
|
@@ -440,7 +440,10 @@ files:
|
|
440
440
|
- lib/features/support/env.rb
|
441
441
|
- lib/features/support/internal_hooks.rb
|
442
442
|
- lib/maze.rb
|
443
|
+
- lib/maze/api/appium/app_manager.rb
|
444
|
+
- lib/maze/api/appium/device_manager.rb
|
443
445
|
- lib/maze/api/appium/file_manager.rb
|
446
|
+
- lib/maze/api/appium/manager.rb
|
444
447
|
- lib/maze/api/cucumber/scenario.rb
|
445
448
|
- lib/maze/api/exit_code.rb
|
446
449
|
- lib/maze/appium_server.rb
|