bugsnag-maze-runner 7.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +74 -0
  4. data/bin/maze-runner +174 -0
  5. data/bin/upload-app +56 -0
  6. data/lib/features/scripts/await-android-emulator.sh +11 -0
  7. data/lib/features/scripts/clear-android-app-data.sh +8 -0
  8. data/lib/features/scripts/force-stop-android-app.sh +8 -0
  9. data/lib/features/scripts/install-android-app.sh +15 -0
  10. data/lib/features/scripts/launch-android-app.sh +38 -0
  11. data/lib/features/scripts/launch-android-emulator.sh +15 -0
  12. data/lib/features/steps/android_steps.rb +51 -0
  13. data/lib/features/steps/app_automator_steps.rb +228 -0
  14. data/lib/features/steps/aws_sam_steps.rb +212 -0
  15. data/lib/features/steps/breadcrumb_steps.rb +80 -0
  16. data/lib/features/steps/browser_steps.rb +93 -0
  17. data/lib/features/steps/build_api_steps.rb +25 -0
  18. data/lib/features/steps/document_server_steps.rb +7 -0
  19. data/lib/features/steps/error_reporting_steps.rb +358 -0
  20. data/lib/features/steps/feature_flag_steps.rb +190 -0
  21. data/lib/features/steps/header_steps.rb +72 -0
  22. data/lib/features/steps/log_steps.rb +29 -0
  23. data/lib/features/steps/multipart_request_steps.rb +142 -0
  24. data/lib/features/steps/network_steps.rb +135 -0
  25. data/lib/features/steps/payload_steps.rb +257 -0
  26. data/lib/features/steps/proxy_steps.rb +34 -0
  27. data/lib/features/steps/query_parameter_steps.rb +31 -0
  28. data/lib/features/steps/request_assertion_steps.rb +186 -0
  29. data/lib/features/steps/runner_steps.rb +428 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/trace_steps.rb +206 -0
  32. data/lib/features/steps/value_steps.rb +119 -0
  33. data/lib/features/support/env.rb +7 -0
  34. data/lib/features/support/internal_hooks.rb +207 -0
  35. data/lib/maze/api/appium/file_manager.rb +29 -0
  36. data/lib/maze/appium_server.rb +112 -0
  37. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  38. data/lib/maze/aws/sam.rb +112 -0
  39. data/lib/maze/aws_public_ip.rb +53 -0
  40. data/lib/maze/bugsnag_config.rb +42 -0
  41. data/lib/maze/checks/assert_check.rb +69 -0
  42. data/lib/maze/checks/noop_check.rb +34 -0
  43. data/lib/maze/client/appium/base_client.rb +131 -0
  44. data/lib/maze/client/appium/bb_client.rb +102 -0
  45. data/lib/maze/client/appium/bb_devices.rb +127 -0
  46. data/lib/maze/client/appium/bs_client.rb +91 -0
  47. data/lib/maze/client/appium/bs_devices.rb +141 -0
  48. data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
  49. data/lib/maze/client/appium/local_client.rb +67 -0
  50. data/lib/maze/client/appium.rb +23 -0
  51. data/lib/maze/client/bb_api_client.rb +102 -0
  52. data/lib/maze/client/bb_client_utils.rb +181 -0
  53. data/lib/maze/client/bs_client_utils.rb +168 -0
  54. data/lib/maze/client/selenium/base_client.rb +15 -0
  55. data/lib/maze/client/selenium/bb_browsers.yml +188 -0
  56. data/lib/maze/client/selenium/bb_client.rb +38 -0
  57. data/lib/maze/client/selenium/bs_browsers.yml +257 -0
  58. data/lib/maze/client/selenium/bs_client.rb +89 -0
  59. data/lib/maze/client/selenium/local_client.rb +16 -0
  60. data/lib/maze/client/selenium.rb +16 -0
  61. data/lib/maze/compare.rb +161 -0
  62. data/lib/maze/configuration.rb +182 -0
  63. data/lib/maze/docker.rb +147 -0
  64. data/lib/maze/document_server.rb +46 -0
  65. data/lib/maze/driver/appium.rb +198 -0
  66. data/lib/maze/driver/browser.rb +124 -0
  67. data/lib/maze/errors.rb +52 -0
  68. data/lib/maze/generator.rb +55 -0
  69. data/lib/maze/helper.rb +122 -0
  70. data/lib/maze/hooks/appium_hooks.rb +55 -0
  71. data/lib/maze/hooks/browser_hooks.rb +15 -0
  72. data/lib/maze/hooks/command_hooks.rb +9 -0
  73. data/lib/maze/hooks/error_code_hook.rb +49 -0
  74. data/lib/maze/hooks/hooks.rb +61 -0
  75. data/lib/maze/http_request.rb +21 -0
  76. data/lib/maze/interactive_cli.rb +173 -0
  77. data/lib/maze/logger.rb +86 -0
  78. data/lib/maze/macos_utils.rb +14 -0
  79. data/lib/maze/maze_output.rb +88 -0
  80. data/lib/maze/network.rb +49 -0
  81. data/lib/maze/option/parser.rb +240 -0
  82. data/lib/maze/option/processor.rb +130 -0
  83. data/lib/maze/option/validator.rb +155 -0
  84. data/lib/maze/option.rb +62 -0
  85. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  86. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  87. data/lib/maze/plugins/error_code_plugin.rb +21 -0
  88. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  89. data/lib/maze/proxy.rb +114 -0
  90. data/lib/maze/request_list.rb +87 -0
  91. data/lib/maze/request_repeater.rb +49 -0
  92. data/lib/maze/retry_handler.rb +67 -0
  93. data/lib/maze/runner.rb +149 -0
  94. data/lib/maze/schemas/OtelTraceSchema.json +390 -0
  95. data/lib/maze/schemas/trace_schema.rb +7 -0
  96. data/lib/maze/schemas/trace_validator.rb +98 -0
  97. data/lib/maze/server.rb +251 -0
  98. data/lib/maze/servlets/base_servlet.rb +27 -0
  99. data/lib/maze/servlets/command_servlet.rb +47 -0
  100. data/lib/maze/servlets/log_servlet.rb +64 -0
  101. data/lib/maze/servlets/reflective_servlet.rb +70 -0
  102. data/lib/maze/servlets/servlet.rb +199 -0
  103. data/lib/maze/servlets/temp.rb +0 -0
  104. data/lib/maze/servlets/trace_servlet.rb +13 -0
  105. data/lib/maze/store.rb +15 -0
  106. data/lib/maze/terminating_server.rb +129 -0
  107. data/lib/maze/timers.rb +51 -0
  108. data/lib/maze/wait.rb +35 -0
  109. data/lib/maze.rb +27 -0
  110. data/lib/utils/deep_merge.rb +17 -0
  111. data/lib/utils/selenium_money_patch.rb +17 -0
  112. 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
@@ -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