ruby_raider 3.13.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/runtime.yml +38 -0
- data/.github/workflows/system_tests.yml +1 -23
- data/.reek.yml +1 -0
- data/.rspec +1 -0
- data/Gemfile +2 -0
- data/README.md +16 -49
- data/RELEASE.md +22 -0
- data/bin/raider +5 -0
- data/lib/commands/scaffolding_commands.rb +2 -25
- data/lib/commands/utility_commands.rb +23 -3
- data/lib/generators/automation/automation_generator.rb +0 -5
- data/lib/generators/automation/templates/component.tt +1 -0
- data/lib/generators/automation/templates/home.tt +2 -2
- data/lib/generators/automation/templates/login.tt +6 -26
- data/lib/generators/automation/templates/page.tt +23 -18
- data/lib/generators/automation/templates/partials/home_page_selector.tt +1 -2
- data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -8
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +1 -2
- data/lib/generators/automation/templates/partials/url_methods.tt +8 -2
- data/lib/generators/automation/templates/partials/visit_method.tt +0 -5
- data/lib/generators/automation/templates/pdp.tt +1 -9
- data/lib/generators/common_generator.rb +0 -2
- data/lib/generators/generator.rb +12 -19
- data/lib/generators/helper_generator.rb +21 -19
- data/lib/generators/infrastructure/templates/azure_pipelines.tt +1 -1
- data/lib/generators/infrastructure/templates/github.tt +2 -3
- data/lib/generators/infrastructure/templates/github_appium.tt +0 -8
- data/lib/generators/invoke_generators.rb +4 -23
- data/lib/generators/menu_generator.rb +1 -9
- data/lib/generators/rspec/templates/accessibility_spec.tt +1 -9
- data/lib/generators/rspec/templates/spec.tt +8 -24
- data/lib/generators/template_renderer/partial_cache.rb +3 -1
- data/lib/generators/template_renderer.rb +1 -1
- data/lib/generators/templates/common/gemfile.tt +3 -10
- data/lib/generators/templates/common/git_ignore.tt +0 -5
- data/lib/generators/templates/common/partials/mobile_config.tt +3 -1
- data/lib/generators/templates/common/partials/web_config.tt +10 -1
- data/lib/generators/templates/common/rakefile.tt +0 -15
- data/lib/generators/templates/common/read_me.tt +357 -250
- data/lib/generators/templates/common/rubocop.tt +0 -2
- data/lib/generators/templates/helpers/driver_helper.tt +8 -0
- data/lib/generators/templates/helpers/gesture_helper.tt +78 -0
- data/lib/generators/templates/helpers/network_helper.tt +521 -0
- data/lib/generators/templates/helpers/partials/quit_driver.tt +0 -2
- data/lib/generators/templates/helpers/partials/screenshot.tt +0 -2
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +5 -2
- data/lib/generators/templates/helpers/screenshot_helper.tt +490 -0
- data/lib/generators/templates/helpers/spec_helper.tt +39 -29
- data/lib/generators/templates/helpers/trace_helper.tt +240 -95
- data/lib/generators/templates/helpers/video_helper.tt +3 -8
- data/lib/ruby_raider/runtime/actionability.rb +129 -0
- data/lib/ruby_raider/runtime/actionable.rb +49 -0
- data/lib/ruby_raider/runtime/by_role.rb +55 -0
- data/lib/ruby_raider/runtime/config.rb +79 -0
- data/lib/ruby_raider/runtime/context.rb +38 -0
- data/lib/ruby_raider/runtime/errors.rb +69 -0
- data/lib/ruby_raider/runtime/finders.rb +73 -0
- data/lib/ruby_raider/runtime/js/actionability.js +134 -0
- data/lib/ruby_raider/runtime/js/role_engine.js +315 -0
- data/lib/ruby_raider/runtime/locator_converter.rb +72 -0
- data/lib/ruby_raider/runtime/matchers/base.rb +222 -0
- data/lib/ruby_raider/runtime/matchers/element.rb +183 -0
- data/lib/ruby_raider/runtime/matchers/page.rb +71 -0
- data/lib/ruby_raider/runtime/matchers/presence.rb +41 -0
- data/lib/ruby_raider/runtime/matchers/retrying_block.rb +104 -0
- data/lib/ruby_raider/runtime/observer.rb +170 -0
- data/lib/ruby_raider/runtime/role_finder.rb +83 -0
- data/lib/ruby_raider/runtime/rspec.rb +78 -0
- data/lib/ruby_raider/runtime/selectors.rb +60 -0
- data/lib/ruby_raider/runtime/strict.rb +142 -0
- data/lib/ruby_raider/runtime/wait.rb +84 -0
- data/lib/ruby_raider/runtime.rb +20 -0
- data/lib/scaffolding/name_normalizer.rb +1 -1
- data/lib/scaffolding/project_detector.rb +2 -28
- data/lib/scaffolding/scaffolding.rb +0 -20
- data/lib/scaffolding/templates/component.tt +0 -14
- data/lib/scaffolding/templates/page_object.tt +2 -14
- data/lib/scaffolding/templates/spec.tt +1 -9
- data/lib/utilities/utilities.rb +10 -8
- data/lib/version +1 -1
- data/ruby_raider.gemspec +2 -1
- data/sig/commands/scaffolding_commands.rbs +0 -2
- data/sig/commands/utility_commands.rbs +6 -0
- data/sig/generators/generator.rbs +0 -4
- data/sig/generators/helper_generator.rbs +0 -1
- data/sig/generators/menu_generator.rbs +0 -1
- data/sig/ruby_raider/runtime/actionability.rbs +25 -0
- data/sig/ruby_raider/runtime/actionable.rbs +13 -0
- data/sig/ruby_raider/runtime/by_role.rbs +15 -0
- data/sig/ruby_raider/runtime/config.rbs +26 -0
- data/sig/ruby_raider/runtime/context.rbs +12 -0
- data/sig/ruby_raider/runtime/errors.rbs +36 -0
- data/sig/ruby_raider/runtime/finders.rbs +26 -0
- data/sig/ruby_raider/runtime/locator_converter.rbs +21 -0
- data/sig/ruby_raider/runtime/matchers/base.rbs +64 -0
- data/sig/ruby_raider/runtime/matchers/element.rbs +78 -0
- data/sig/ruby_raider/runtime/matchers/page.rbs +38 -0
- data/sig/ruby_raider/runtime/matchers/presence.rbs +21 -0
- data/sig/ruby_raider/runtime/matchers/retrying_block.rbs +27 -0
- data/sig/ruby_raider/runtime/observer.rbs +46 -0
- data/sig/ruby_raider/runtime/role_finder.rbs +18 -0
- data/sig/ruby_raider/runtime/selectors.rbs +21 -0
- data/sig/ruby_raider/runtime/strict.rbs +35 -0
- data/sig/ruby_raider/runtime/wait.rbs +17 -0
- data/sig/ruby_raider/runtime.rbs +2 -0
- data/sig/scaffolding/project_detector.rbs +0 -4
- data/sig/scaffolding/scaffolding.rbs +0 -5
- data/sig/utilities/utilities.rbs +2 -1
- data/sig/vendor/selenium_webdriver.rbs +71 -0
- data/spec/commands/raider_commands_spec.rb +38 -3
- data/spec/commands/scaffolding_commands_spec.rb +27 -0
- data/spec/commands/utility_commands_spec.rb +53 -0
- data/spec/generators/gesture_helper_spec.rb +123 -0
- data/spec/generators/network_helper_spec.rb +634 -0
- data/spec/generators/screenshot_helper_spec.rb +776 -0
- data/spec/generators/template_renderer_spec.rb +0 -4
- data/spec/generators/trace_helper_spec.rb +489 -97
- data/spec/generators/video_helper_spec.rb +0 -9
- data/spec/integration/commands/browser_update_after_creation_spec.rb +0 -8
- data/spec/integration/commands/scaffolding_commands_spec.rb +12 -70
- data/spec/integration/commands/utility_commands_spec.rb +19 -41
- data/spec/integration/content/azure_content_spec.rb +2 -6
- data/spec/integration/content/ci_content_spec.rb +1 -55
- data/spec/integration/content/common_content_spec.rb +1 -118
- data/spec/integration/content/config_content_spec.rb +34 -31
- data/spec/integration/content/gemfile_content_spec.rb +7 -72
- data/spec/integration/content/helper_content_spec.rb +105 -99
- data/spec/integration/content/page_content_spec.rb +85 -87
- data/spec/integration/content/readme_content_spec.rb +134 -75
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/content/rubocop_content_spec.rb +1 -1
- data/spec/integration/content/syntax_validation_spec.rb +2 -2
- data/spec/integration/content/test_content_spec.rb +23 -147
- data/spec/integration/end_to_end_features_spec.rb +1 -43
- data/spec/integration/end_to_end_spec.rb +319 -326
- data/spec/integration/generators/automation_generator_spec.rb +3 -37
- data/spec/integration/generators/axe_addon_spec.rb +2 -35
- data/spec/integration/generators/azure_generator_spec.rb +1 -1
- data/spec/integration/generators/common_generator_spec.rb +1 -40
- data/spec/integration/generators/config_features_spec.rb +1 -24
- data/spec/integration/generators/github_generator_spec.rb +0 -12
- data/spec/integration/generators/helpers_generator_spec.rb +74 -80
- data/spec/integration/generators/invoke_generators_spec.rb +54 -45
- data/spec/integration/generators/rspec_generator_spec.rb +0 -6
- data/spec/integration/scaffolding_e2e_spec.rb +17 -179
- data/spec/integration/spec_helper.rb +4 -6
- data/spec/integration/trace_format_checker.rb +11 -2
- data/spec/integration/trace_page_server.rb +72 -5
- data/spec/menus/menu_generator_spec.rb +14 -39
- data/spec/runtime/actionability_spec.rb +207 -0
- data/spec/runtime/actionable_spec.rb +125 -0
- data/spec/runtime/browser/actionability_script_browser_spec.rb +150 -0
- data/spec/runtime/browser/actionable_browser_spec.rb +98 -0
- data/spec/runtime/browser/finders_browser_spec.rb +148 -0
- data/spec/runtime/browser/matchers_browser_spec.rb +67 -0
- data/spec/runtime/browser/retrying_block_browser_spec.rb +37 -0
- data/spec/runtime/browser/role_engine_browser_spec.rb +184 -0
- data/spec/runtime/browser/strict_browser_spec.rb +38 -0
- data/spec/runtime/by_role_spec.rb +52 -0
- data/spec/runtime/config_spec.rb +79 -0
- data/spec/runtime/context_spec.rb +73 -0
- data/spec/runtime/errors_spec.rb +89 -0
- data/spec/runtime/finders_spec.rb +165 -0
- data/spec/runtime/fixtures/actionability.html +70 -0
- data/spec/runtime/fixtures/locators.html +46 -0
- data/spec/runtime/fixtures/matchers.html +33 -0
- data/spec/runtime/fixtures/roles.html +162 -0
- data/spec/runtime/matchers/base_spec.rb +165 -0
- data/spec/runtime/matchers/element_spec.rb +277 -0
- data/spec/runtime/matchers/page_spec.rb +102 -0
- data/spec/runtime/matchers/presence_spec.rb +59 -0
- data/spec/runtime/matchers/retrying_block_spec.rb +165 -0
- data/spec/runtime/observer_spec.rb +341 -0
- data/spec/runtime/role_finder_spec.rb +156 -0
- data/spec/runtime/role_query_spec.rb +75 -0
- data/spec/runtime/selectors_spec.rb +135 -0
- data/spec/runtime/strict_spec.rb +257 -0
- data/spec/runtime/support/browser.rb +48 -0
- data/spec/runtime/support/fake_bridge.rb +117 -0
- data/spec/runtime/wait_spec.rb +71 -0
- data/spec/scaffolding/name_normalizer_spec.rb +3 -2
- data/spec/scaffolding/scaffold_project_detector_spec.rb +4 -43
- data/spec/system/selenium_spec.rb +1 -5
- data/spec/system/support/system_test_helper.rb +2 -5
- data/spec/utilities/headless_config_spec.rb +0 -7
- data/spec/utilities/utilities_spec.rb +24 -0
- metadata +113 -48
- data/lib/generators/automation/templates/header.tt +0 -9
- data/lib/generators/cucumber/cucumber_generator.rb +0 -58
- data/lib/generators/cucumber/templates/accessibility_feature.tt +0 -5
- data/lib/generators/cucumber/templates/accessibility_steps.tt +0 -17
- data/lib/generators/cucumber/templates/cucumber.tt +0 -1
- data/lib/generators/cucumber/templates/env.tt +0 -7
- data/lib/generators/cucumber/templates/feature.tt +0 -21
- data/lib/generators/cucumber/templates/partials/appium_env.tt +0 -26
- data/lib/generators/cucumber/templates/partials/driver_world.tt +0 -18
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +0 -20
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +0 -120
- data/lib/generators/cucumber/templates/partials/watir_env.tt +0 -48
- data/lib/generators/cucumber/templates/partials/watir_world.tt +0 -4
- data/lib/generators/cucumber/templates/partials/web_steps.tt +0 -28
- data/lib/generators/cucumber/templates/steps.tt +0 -5
- data/lib/generators/cucumber/templates/user_factory.tt +0 -15
- data/lib/generators/cucumber/templates/world.tt +0 -7
- data/lib/generators/templates/helpers/browser_helper.tt +0 -31
- data/lib/generators/templates/helpers/raider_element.tt +0 -159
- data/lib/generators/templates/helpers/wait_helper.tt +0 -611
- data/lib/scaffolding/templates/feature.tt +0 -6
- data/lib/scaffolding/templates/steps.tt +0 -19
- data/sig/generators/cucumber/cucumber_generator.rbs +0 -12
- data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +0 -175
- data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +0 -24
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +0 -37
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +0 -33
- data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +0 -174
- data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +0 -37
- data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +0 -33
- data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +0 -49
- data/spec/integration/content/cucumber_reporter_content_spec.rb +0 -161
- data/spec/integration/generators/cucumber_generator_spec.rb +0 -100
- data/spec/integration/helpers/wait_helper_mobile_spec.rb +0 -281
- data/spec/integration/helpers/wait_helper_spec.rb +0 -679
- data/spec/integration/helpers/wait_helper_support.rb +0 -183
- data/spec/system/watir_spec.rb +0 -38
|
@@ -3,28 +3,28 @@
|
|
|
3
3
|
require 'cgi/escape'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
require 'json'
|
|
6
|
+
require 'ruby_raider/runtime'
|
|
6
7
|
require 'selenium-webdriver'
|
|
7
8
|
require 'tmpdir'
|
|
8
9
|
require 'yaml'
|
|
9
10
|
require 'zlib'
|
|
10
11
|
|
|
11
|
-
# Records a trace of each test: one step per
|
|
12
|
+
# Records a trace of each test: one step per element action and navigation, with
|
|
12
13
|
# a screenshot and the page HTML after each state-changing step. stop(keep: true)
|
|
13
14
|
# writes it as trace.zip in the raider-trace v1 format, which Raider Desktop and
|
|
14
15
|
# Hideout open. Any capture error prints one [trace] line and turns tracing off for
|
|
15
16
|
# the rest of the run; it never raises into a test. Something only left out (a
|
|
16
17
|
# screenshot, or network on a browser without BiDi) prints at most one notice. TRACE=off skips tracing and
|
|
17
18
|
# TRACE=on keeps the trace of passing tests too.
|
|
18
|
-
# The Recorder follows
|
|
19
|
-
# trace records only navigation and the failure. Navigation comes from TraceHelper.listener,
|
|
19
|
+
# The Recorder follows Raider.observer. Navigation comes from TraceHelper.listener,
|
|
20
20
|
# and network and console from passive WebDriver BiDi events (chrome, edge and firefox).
|
|
21
21
|
module TraceHelper
|
|
22
22
|
DEFAULTS = { 'enabled' => true, 'max_steps' => 200, 'max_mb' => 20 }.freeze
|
|
23
23
|
PRODUCER = { name: 'ruby_raider', version: '<%= ruby_raider_version %>' }.freeze
|
|
24
24
|
# Only these get a screenshot and a snapshot; query actions cost almost nothing.
|
|
25
|
-
MEDIA_ACTIONS = %w[navigate click send_keys clear submit
|
|
26
|
-
# Only these record what was typed
|
|
27
|
-
TYPED_ACTIONS = %w[send_keys
|
|
25
|
+
MEDIA_ACTIONS = %w[navigate click send_keys clear submit].freeze
|
|
26
|
+
# Only these record what was typed, as value (null when secret).
|
|
27
|
+
TYPED_ACTIONS = %w[send_keys].freeze
|
|
28
28
|
SECRET_ENV_PREFIX = 'RAIDER_SECRET_'
|
|
29
29
|
# The driver helper sets web_socket_url for these while tracing is on.
|
|
30
30
|
BIDI_BROWSERS = %w[chrome edge firefox].freeze
|
|
@@ -35,6 +35,7 @@ module TraceHelper
|
|
|
35
35
|
@warned = false
|
|
36
36
|
@disable_warned = false
|
|
37
37
|
@active_recorder = nil
|
|
38
|
+
@flags = Mutex.new
|
|
38
39
|
|
|
39
40
|
class << self
|
|
40
41
|
attr_accessor :active_recorder
|
|
@@ -54,7 +55,7 @@ module TraceHelper
|
|
|
54
55
|
# True while tracing is on for a browser that speaks WebDriver BiDi (Edge reports MicrosoftEdge).
|
|
55
56
|
def bidi?(browser)
|
|
56
57
|
name = browser.to_s.downcase.delete_prefix('microsoft')
|
|
57
|
-
enabled? && BIDI_BROWSERS.any? { |bidi_browser| name.start_with?(bidi_browser) }
|
|
58
|
+
enabled? && ENV['BIDI'].to_s.downcase != 'off' && BIDI_BROWSERS.any? { |bidi_browser| name.start_with?(bidi_browser) }
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
# Passed to Selenium through the driver helper's fan-out listener when the driver is created.
|
|
@@ -69,17 +70,19 @@ module TraceHelper
|
|
|
69
70
|
return null_recorder unless enabled?
|
|
70
71
|
|
|
71
72
|
recorder = Recorder.new(driver, screenshot: screenshot, snapshot: snapshot)
|
|
72
|
-
|
|
73
|
+
Raider.observer = recorder
|
|
74
|
+
# Lazy: the project may have removed network_helper.
|
|
75
|
+
NetworkHelper.observer = MockMarks if defined?(NetworkHelper)
|
|
73
76
|
recorder
|
|
74
77
|
rescue StandardError => e
|
|
75
78
|
disable!("could not create a recorder: #{e.message}")
|
|
76
79
|
null_recorder
|
|
77
80
|
end
|
|
78
81
|
|
|
79
|
-
# Unhooks the recorder from
|
|
82
|
+
# Unhooks the recorder from Raider.observer and the navigation listener.
|
|
80
83
|
def release(recorder)
|
|
81
84
|
@active_recorder = nil if @active_recorder.equal?(recorder)
|
|
82
|
-
|
|
85
|
+
Raider.observer = nil if Raider.observer.equal?(recorder)
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
# The first reason tracing stopped always gets its own [trace] line, even after a notice.
|
|
@@ -87,22 +90,33 @@ module TraceHelper
|
|
|
87
90
|
@disabled = true
|
|
88
91
|
release(@active_recorder)
|
|
89
92
|
detach_observer
|
|
90
|
-
return nil
|
|
93
|
+
return nil unless claim(:@disable_warned)
|
|
91
94
|
|
|
92
|
-
@disable_warned = true
|
|
93
95
|
say("#{reason}; tracing disabled for this run")
|
|
94
96
|
nil
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
# At most one notice per run, for something left out of a trace while tracing goes on.
|
|
98
100
|
def warn_once(message)
|
|
99
|
-
return nil
|
|
101
|
+
return nil unless claim(:@warned)
|
|
100
102
|
|
|
101
|
-
@warned = true
|
|
102
103
|
say(message)
|
|
103
104
|
nil
|
|
104
105
|
end
|
|
105
106
|
|
|
107
|
+
# One notice per run for events dropped as malformed, apart from warn_once so neither hides the other.
|
|
108
|
+
def warn_dropped_event(message)
|
|
109
|
+
return nil unless claim(:@dropped_warned)
|
|
110
|
+
|
|
111
|
+
say(message)
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A real number that is neither NaN nor infinite; a duration or a timestamp is built only from these.
|
|
116
|
+
def finite_number?(value)
|
|
117
|
+
value.is_a?(Numeric) && value.real? && value.finite?
|
|
118
|
+
end
|
|
119
|
+
|
|
106
120
|
# Above 9999 is taken as 9999, silently; zero or less (or not a number) as the default.
|
|
107
121
|
def max_steps
|
|
108
122
|
value = config['max_steps'].to_i
|
|
@@ -141,7 +155,7 @@ module TraceHelper
|
|
|
141
155
|
end
|
|
142
156
|
|
|
143
157
|
def detach_observer
|
|
144
|
-
|
|
158
|
+
Raider.observer = nil if Raider.observer.is_a?(Recorder)
|
|
145
159
|
end
|
|
146
160
|
|
|
147
161
|
def trace_config
|
|
@@ -151,6 +165,15 @@ module TraceHelper
|
|
|
151
165
|
{}
|
|
152
166
|
end
|
|
153
167
|
|
|
168
|
+
# Atomically takes a once-only flag; true for the first caller only. Events arrive on websocket threads.
|
|
169
|
+
def claim(flag)
|
|
170
|
+
@flags.synchronize do
|
|
171
|
+
next false if instance_variable_get(flag)
|
|
172
|
+
|
|
173
|
+
instance_variable_set(flag, true)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
154
177
|
# $stderr.puts, not Kernel#warn: warn is silenced under -W0.
|
|
155
178
|
# rubocop:disable Style/StderrPuts
|
|
156
179
|
def say(message)
|
|
@@ -159,8 +182,40 @@ module TraceHelper
|
|
|
159
182
|
# rubocop:enable Style/StderrPuts
|
|
160
183
|
end
|
|
161
184
|
|
|
162
|
-
#
|
|
163
|
-
|
|
185
|
+
# Where an action was called from, for the observer.
|
|
186
|
+
module Observation
|
|
187
|
+
# helpers/ by its given and its real path (__dir__ is the real one), and the runtime's own directory:
|
|
188
|
+
# the trace, driver and runtime files are never the line that called an action or a navigation.
|
|
189
|
+
HELPER_DIRS = [File.dirname(__FILE__), __dir__, Raider::Observer::LIB_DIR].uniq.map { |dir| File.join(dir, '') }.freeze
|
|
190
|
+
|
|
191
|
+
# The first frame outside helpers/, Ruby and gems, as { file:, line: } (file relative
|
|
192
|
+
# to the working directory when inside it), or nil.
|
|
193
|
+
def self.source
|
|
194
|
+
frame = caller_locations.find { |location| project?(location.absolute_path || location.path) }
|
|
195
|
+
return unless frame
|
|
196
|
+
|
|
197
|
+
{ file: relative(frame.absolute_path || frame.path), line: frame.lineno }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.project?(path)
|
|
201
|
+
return false if path.start_with?('<internal:') || HELPER_DIRS.any? { |dir| path.start_with?(dir) }
|
|
202
|
+
|
|
203
|
+
library_paths.none? { |root| path.start_with?(root) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.library_paths
|
|
207
|
+
@library_paths ||= [*RbConfig::CONFIG.values_at('rubylibdir', 'rubyarchdir'), *Gem.path]
|
|
208
|
+
.compact.map { |dir| File.join(dir, '') }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def self.relative(path)
|
|
212
|
+
root = File.join(Dir.pwd, '')
|
|
213
|
+
path.start_with?(root) ? path.delete_prefix(root) : path
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# One step of the timeline. parent is always nil (the v1 group kind stays readable but is no longer
|
|
218
|
+
# produced); the index is given when the trace is written, after max_steps has dropped steps.
|
|
164
219
|
# element is the format's { tag, label, role }; secret is true, false, or nil while the field is unknown.
|
|
165
220
|
Step = Struct.new(:seq, :parent, :kind, :action, :locator, :url, :page_title, :started_at, :clock, :duration_ms,
|
|
166
221
|
:waited_ms, :retries, :status, :error, :source, :rect, :screenshot, :snapshot, :element,
|
|
@@ -171,12 +226,6 @@ module TraceHelper
|
|
|
171
226
|
self.duration_ms = ((TraceHelper.monotonic_now - clock) * 1000).round
|
|
172
227
|
end
|
|
173
228
|
|
|
174
|
-
def within?(group)
|
|
175
|
-
step = parent
|
|
176
|
-
step = step.parent until step.nil? || step.equal?(group)
|
|
177
|
-
!step.nil?
|
|
178
|
-
end
|
|
179
|
-
|
|
180
229
|
def as_json(index, parent_index, media)
|
|
181
230
|
json = { index: index, parent_index: parent_index, kind: kind, action: action, locator: locator }
|
|
182
231
|
json[:element] = element if element
|
|
@@ -194,11 +243,13 @@ module TraceHelper
|
|
|
194
243
|
end
|
|
195
244
|
end
|
|
196
245
|
|
|
197
|
-
# Follows a test:
|
|
246
|
+
# Follows a test: the runtime reports element actions to Raider.observer and the
|
|
198
247
|
# navigation listener calls navigated. Media goes to its own tmpdir until stop.
|
|
199
248
|
# BiDi events arrive on Selenium's websocket threads, so the event lists are locked.
|
|
200
249
|
class Recorder
|
|
201
250
|
SNAPSHOT = :page_source.to_proc
|
|
251
|
+
# Requests awaiting their response, and responses awaiting their request: a page cannot grow either past this.
|
|
252
|
+
MAX_PENDING = 1_000
|
|
202
253
|
|
|
203
254
|
attr_reader :dir, :capabilities
|
|
204
255
|
|
|
@@ -224,16 +275,30 @@ module TraceHelper
|
|
|
224
275
|
discard
|
|
225
276
|
end
|
|
226
277
|
|
|
227
|
-
# Returns the step as the token
|
|
228
|
-
# The locator has no label: a page object's element method has returned
|
|
229
|
-
|
|
278
|
+
# Returns the step as the token the runtime hands back (nil when not recording, so no ready or finished
|
|
279
|
+
# follows). args: send_keys' keys. The locator has no label: a page object's element method has returned
|
|
280
|
+
# before the action runs.
|
|
281
|
+
def action_started(action:, locator:, within:, args: nil)
|
|
230
282
|
record('action') do
|
|
231
|
-
step = add(kind: 'action', action: action.to_s, locator: trace_locator(locator, within),
|
|
283
|
+
step = add(kind: 'action', action: action.to_s, locator: trace_locator(locator, within),
|
|
284
|
+
source: TraceHelper::Observation.source)
|
|
232
285
|
typed(step, Array(args)) if TYPED_ACTIONS.include?(step.action)
|
|
233
286
|
step
|
|
234
287
|
end
|
|
235
288
|
end
|
|
236
289
|
|
|
290
|
+
# match_screenshot compared the page with a baseline. The PNG it took is the step's screenshot,
|
|
291
|
+
# so no second screenshot is taken.
|
|
292
|
+
def screenshot_compared(name:, status:, png:, error: nil)
|
|
293
|
+
record('action') do
|
|
294
|
+
step = add(kind: 'action', action: 'match_screenshot', locator: nil, source: navigation_source)
|
|
295
|
+
step.name = name
|
|
296
|
+
step.finish(status, error)
|
|
297
|
+
step.screenshot = media(step, 'screenshot') { [png, 'png'] }
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
237
302
|
# The element is ready and the action is about to run: its rect, what it is, and whether it is secret.
|
|
238
303
|
# Query actions skip it, to stay cheap.
|
|
239
304
|
def action_ready(step, element:)
|
|
@@ -254,49 +319,22 @@ module TraceHelper
|
|
|
254
319
|
end
|
|
255
320
|
end
|
|
256
321
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
# A new pass of eventually: only the last pass's steps are kept, and the
|
|
266
|
-
# group's error is the last one an earlier pass raised.
|
|
267
|
-
def group_retried(group, error: nil)
|
|
268
|
-
record('group') do
|
|
269
|
-
dropped, @steps = @steps.partition { |step| step.within?(group) }
|
|
270
|
-
dropped.each { |step| delete_media(step) }
|
|
271
|
-
group.error = error
|
|
272
|
-
nil
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def group_finished(group, status:, retries:, error:)
|
|
277
|
-
record('group') do
|
|
278
|
-
@groups.reject! { |open| open.equal?(group) }
|
|
279
|
-
group.finish(status, error || group.error)
|
|
280
|
-
group.retries = retries
|
|
322
|
+
# A navigation has begun. Its step stays pending until navigated, so the requests the page makes
|
|
323
|
+
# while loading are credited to it. A driver.get that fails never reaches navigated.
|
|
324
|
+
def navigating(url)
|
|
325
|
+
record('navigation') do
|
|
326
|
+
finish_navigation(:failed)
|
|
327
|
+
@navigation = add(kind: 'navigate', action: 'navigate', locator: nil, source: navigation_source)
|
|
328
|
+
@navigation.url = url if url
|
|
281
329
|
nil
|
|
282
330
|
end
|
|
283
331
|
end
|
|
284
332
|
|
|
285
|
-
# A Cucumber step is a group named by its keyword and text; the actions it runs become its children.
|
|
286
|
-
def step_started(name:, source:)
|
|
287
|
-
group = group_started(action: :cucumber_step, source: source)
|
|
288
|
-
group.name = name if group.is_a?(Step)
|
|
289
|
-
group
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def step_finished(group, status:, error:)
|
|
293
|
-
group_finished(group, status: status, retries: 0, error: error) if group.is_a?(Step)
|
|
294
|
-
end
|
|
295
|
-
|
|
296
333
|
def navigated(url)
|
|
297
334
|
record('navigation') do
|
|
298
|
-
step = add(kind: 'navigate', action: 'navigate', locator: nil, source: navigation_source)
|
|
299
|
-
|
|
335
|
+
step = @navigation || add(kind: 'navigate', action: 'navigate', locator: nil, source: navigation_source)
|
|
336
|
+
@navigation = nil
|
|
337
|
+
step.url = url if url
|
|
300
338
|
step.finish(:passed, nil)
|
|
301
339
|
capture_step_media(step)
|
|
302
340
|
nil
|
|
@@ -320,6 +358,8 @@ module TraceHelper
|
|
|
320
358
|
entry = event(:network, hash)
|
|
321
359
|
early = @lock.synchronize do
|
|
322
360
|
@requests[request] = [entry, at]
|
|
361
|
+
@requests.shift while @requests.size > MAX_PENDING
|
|
362
|
+
entry.update(@early_marks.delete(request) || {})
|
|
323
363
|
@early.delete(request)
|
|
324
364
|
end
|
|
325
365
|
network_completed(request, *early) if early
|
|
@@ -331,13 +371,32 @@ module TraceHelper
|
|
|
331
371
|
def network_completed(request, fields, at = nil)
|
|
332
372
|
record('network') do
|
|
333
373
|
entry, started = @lock.synchronize do
|
|
334
|
-
@requests.delete(request).tap { |found|
|
|
374
|
+
@requests.delete(request).tap { |found| remember_early(request, fields, at) unless found }
|
|
335
375
|
end
|
|
336
376
|
complete(entry, fields, at, started) if entry
|
|
337
377
|
nil
|
|
338
378
|
end
|
|
339
379
|
end
|
|
340
380
|
|
|
381
|
+
# A mocked request, marked by NetworkHelper before it answers. The entry stays pending: its
|
|
382
|
+
# completion still arrives, and its status wins when it has one. A mark that comes before
|
|
383
|
+
# network_started is kept until the entry exists.
|
|
384
|
+
def network_marked(request, fields)
|
|
385
|
+
record('network') do
|
|
386
|
+
fields = fields.transform_keys(&:to_s)
|
|
387
|
+
@lock.synchronize do
|
|
388
|
+
entry = @requests[request]&.first
|
|
389
|
+
if entry
|
|
390
|
+
entry.update(fields)
|
|
391
|
+
else
|
|
392
|
+
@early_marks[request] = fields
|
|
393
|
+
@early_marks.shift while @early_marks.size > MAX_PENDING
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
nil
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
341
400
|
# hash: a console[] entry of the format; step_index is filled in when written.
|
|
342
401
|
def console_event(hash)
|
|
343
402
|
record('console') do
|
|
@@ -371,6 +430,7 @@ module TraceHelper
|
|
|
371
430
|
TraceHelper.release(self)
|
|
372
431
|
return discard unless keep && TraceHelper.enabled?
|
|
373
432
|
|
|
433
|
+
finish_navigation(:failed, test[:error])
|
|
374
434
|
failure(test[:error])
|
|
375
435
|
write(test)
|
|
376
436
|
rescue StandardError => e
|
|
@@ -390,11 +450,12 @@ module TraceHelper
|
|
|
390
450
|
|
|
391
451
|
def reset
|
|
392
452
|
@steps = []
|
|
393
|
-
@
|
|
453
|
+
@navigation = nil
|
|
394
454
|
@lock.synchronize do
|
|
395
455
|
@events = []
|
|
396
456
|
@requests = {}
|
|
397
457
|
@early = {}
|
|
458
|
+
@early_marks = {}
|
|
398
459
|
end
|
|
399
460
|
@bidi = false
|
|
400
461
|
@seq = 0
|
|
@@ -418,7 +479,7 @@ module TraceHelper
|
|
|
418
479
|
end
|
|
419
480
|
|
|
420
481
|
# Runs a hook while recording. A hook that fails turns tracing off instead of
|
|
421
|
-
# raising into
|
|
482
|
+
# raising into the runtime (which would warn a second time).
|
|
422
483
|
def record(what)
|
|
423
484
|
return nil unless @recording && TraceHelper.enabled?
|
|
424
485
|
|
|
@@ -428,8 +489,9 @@ module TraceHelper
|
|
|
428
489
|
end
|
|
429
490
|
|
|
430
491
|
def add(kind:, action:, locator:, source:)
|
|
492
|
+
finish_navigation(:failed) unless kind == 'navigate'
|
|
431
493
|
@seq += 1
|
|
432
|
-
step = Step.new(seq: @seq, parent:
|
|
494
|
+
step = Step.new(seq: @seq, parent: nil, kind: kind, action: action, locator: locator, source: source,
|
|
433
495
|
url: @page[:url], page_title: @page[:page_title], started_at: TraceHelper.timestamp,
|
|
434
496
|
clock: TraceHelper.monotonic_now, waited_ms: 0, retries: 0)
|
|
435
497
|
@steps << step
|
|
@@ -437,14 +499,35 @@ module TraceHelper
|
|
|
437
499
|
step
|
|
438
500
|
end
|
|
439
501
|
|
|
502
|
+
# A navigation still pending when the next step starts, or at stop, never completed. At stop the test's
|
|
503
|
+
# error goes to it, so failure does not add a second step for the same exception.
|
|
504
|
+
def finish_navigation(status, error = nil)
|
|
505
|
+
return unless @navigation
|
|
506
|
+
|
|
507
|
+
@navigation.finish(status, error)
|
|
508
|
+
@navigation = nil
|
|
509
|
+
end
|
|
510
|
+
|
|
440
511
|
def drop_oldest_step
|
|
441
|
-
|
|
512
|
+
step = @steps.shift
|
|
513
|
+
@navigation = nil if step.equal?(@navigation)
|
|
514
|
+
delete_media(step)
|
|
442
515
|
@truncated[:steps] += 1
|
|
443
516
|
end
|
|
444
517
|
|
|
518
|
+
# Called inside the lock.
|
|
519
|
+
def remember_early(request, fields, at)
|
|
520
|
+
@early[request] = [fields, at]
|
|
521
|
+
@early.shift while @early.size > MAX_PENDING
|
|
522
|
+
end
|
|
523
|
+
|
|
445
524
|
def complete(entry, fields, finished, started)
|
|
446
|
-
|
|
447
|
-
|
|
525
|
+
duration = finished - started if TraceHelper.finite_number?(finished) && TraceHelper.finite_number?(started)
|
|
526
|
+
fields = fields.merge(duration_ms: duration.round) if TraceHelper.finite_number?(duration)
|
|
527
|
+
@lock.synchronize do
|
|
528
|
+
fields = fields.reject { |key, value| key == :status && value.nil? && entry['status'] }
|
|
529
|
+
entry.update(fields.transform_keys(&:to_s))
|
|
530
|
+
end
|
|
448
531
|
end
|
|
449
532
|
|
|
450
533
|
# Returns the stored entry, which network_completed may still fill in.
|
|
@@ -463,12 +546,12 @@ module TraceHelper
|
|
|
463
546
|
end
|
|
464
547
|
|
|
465
548
|
def navigation_source
|
|
466
|
-
|
|
549
|
+
TraceHelper::Observation.source
|
|
467
550
|
end
|
|
468
551
|
|
|
469
|
-
# The value as typed: strings as they are, keys such as :enter as {enter}
|
|
552
|
+
# The value as typed: strings as they are, keys such as :enter as {enter}.
|
|
470
553
|
def typed(step, args)
|
|
471
|
-
shown =
|
|
554
|
+
shown = args.flatten.map { |key| key_name(key) }.join
|
|
472
555
|
step.value = shown
|
|
473
556
|
step.typed_strings = [shown, *args.flatten.grep(String)].reject(&:empty?).uniq
|
|
474
557
|
step.secret = true if step.typed_strings.any? { |each_value| TraceHelper.secret_value?(each_value) }
|
|
@@ -526,7 +609,7 @@ module TraceHelper
|
|
|
526
609
|
end
|
|
527
610
|
|
|
528
611
|
# A test that raised outside every kept step gets a last failure step. Steps dropped by
|
|
529
|
-
#
|
|
612
|
+
# max_steps own nothing, so their error still gets a step.
|
|
530
613
|
def failure(error)
|
|
531
614
|
return if error.nil? || @steps.any? { |step| step.error.equal?(error) }
|
|
532
615
|
|
|
@@ -602,9 +685,13 @@ module TraceHelper
|
|
|
602
685
|
{ name: nil, version: nil }
|
|
603
686
|
end
|
|
604
687
|
|
|
688
|
+
# The inner size: the outer window size includes the browser chrome, so it would not match
|
|
689
|
+
# the rects or the viewport screenshot.
|
|
605
690
|
def viewport
|
|
606
|
-
|
|
607
|
-
{ width:
|
|
691
|
+
width, height = @driver.execute_script('return [window.innerWidth, window.innerHeight]')
|
|
692
|
+
return { width: nil, height: nil } unless width.is_a?(Numeric) && height.is_a?(Numeric)
|
|
693
|
+
|
|
694
|
+
{ width: width, height: height }
|
|
608
695
|
rescue StandardError
|
|
609
696
|
{ width: nil, height: nil }
|
|
610
697
|
end
|
|
@@ -748,6 +835,10 @@ module TraceHelper
|
|
|
748
835
|
nil
|
|
749
836
|
end
|
|
750
837
|
|
|
838
|
+
def navigating(_url)
|
|
839
|
+
nil
|
|
840
|
+
end
|
|
841
|
+
|
|
751
842
|
def navigated(_url)
|
|
752
843
|
nil
|
|
753
844
|
end
|
|
@@ -764,6 +855,10 @@ module TraceHelper
|
|
|
764
855
|
nil
|
|
765
856
|
end
|
|
766
857
|
|
|
858
|
+
def network_marked(_request, _fields)
|
|
859
|
+
nil
|
|
860
|
+
end
|
|
861
|
+
|
|
767
862
|
def console_event(_hash)
|
|
768
863
|
nil
|
|
769
864
|
end
|
|
@@ -966,11 +1061,24 @@ module TraceHelper
|
|
|
966
1061
|
end
|
|
967
1062
|
end
|
|
968
1063
|
|
|
969
|
-
# What comes from the browser beside
|
|
1064
|
+
# What comes from the browser beside the element actions: navigation from Selenium's event listener,
|
|
970
1065
|
# network and console from WebDriver BiDi, and the screenshot.
|
|
971
1066
|
module TraceHelper
|
|
972
1067
|
# Selenium calls these after each navigation (through the driver helper's fan-out listener).
|
|
973
1068
|
class Listener < Selenium::WebDriver::Support::AbstractEventListener
|
|
1069
|
+
# Before, so the requests of the load belong to the navigate step. If the navigation fails, after is never called.
|
|
1070
|
+
def before_navigate_to(url, _driver)
|
|
1071
|
+
navigating(url)
|
|
1072
|
+
end
|
|
1073
|
+
|
|
1074
|
+
def before_navigate_back(_driver)
|
|
1075
|
+
navigating(nil)
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
def before_navigate_forward(_driver)
|
|
1079
|
+
navigating(nil)
|
|
1080
|
+
end
|
|
1081
|
+
|
|
974
1082
|
def after_navigate_to(url, _driver)
|
|
975
1083
|
navigated(url)
|
|
976
1084
|
end
|
|
@@ -986,12 +1094,28 @@ module TraceHelper
|
|
|
986
1094
|
|
|
987
1095
|
private
|
|
988
1096
|
|
|
1097
|
+
def navigating(url)
|
|
1098
|
+
TraceHelper.active_recorder&.navigating(url)
|
|
1099
|
+
nil
|
|
1100
|
+
end
|
|
1101
|
+
|
|
989
1102
|
def navigated(url)
|
|
990
1103
|
TraceHelper.active_recorder&.navigated(url)
|
|
991
1104
|
nil
|
|
992
1105
|
end
|
|
993
1106
|
end
|
|
994
1107
|
|
|
1108
|
+
# NetworkHelper's observer: marks the trace entry of a request it answers without the network.
|
|
1109
|
+
module MockMarks
|
|
1110
|
+
def self.request_mocked(request_id:, redirect_count:, outcome:, status: nil)
|
|
1111
|
+
fields = { mocked: outcome }
|
|
1112
|
+
fields[:status] = status if status
|
|
1113
|
+
TraceHelper.active_recorder&.network_marked([request_id, redirect_count], fields)
|
|
1114
|
+
rescue StandardError => e
|
|
1115
|
+
TraceHelper.warn_dropped_event("dropped a mocked-request mark: #{e.class}")
|
|
1116
|
+
end
|
|
1117
|
+
end
|
|
1118
|
+
|
|
995
1119
|
# Passive BiDi: one session.subscribe per driver, never network.addIntercept, which would
|
|
996
1120
|
# pause every request. Events go to the recorder last attached to that driver. Only the
|
|
997
1121
|
# method, URL, status, MIME type, timing, size and error text are kept: no headers, no bodies.
|
|
@@ -1015,31 +1139,34 @@ module TraceHelper
|
|
|
1015
1139
|
def self.subscribe(bidi)
|
|
1016
1140
|
bidi.session.subscribe(HANDLERS.keys)
|
|
1017
1141
|
link = Link.new
|
|
1018
|
-
HANDLERS.each { |event, handler| bidi.add_callback(event) { |params| deliver(link, handler, params) } }
|
|
1142
|
+
HANDLERS.each { |event, handler| bidi.add_callback(event) { |params| deliver(link, event, handler, params) } }
|
|
1019
1143
|
link
|
|
1020
1144
|
end
|
|
1021
1145
|
|
|
1022
|
-
# On a websocket thread, which swallows errors
|
|
1023
|
-
|
|
1146
|
+
# On a websocket thread, which swallows errors. One event we cannot read is dropped with a notice;
|
|
1147
|
+
# tracing carries on, since the next one is usually fine.
|
|
1148
|
+
def self.deliver(link, event, handler, params)
|
|
1024
1149
|
recorder = link.recorder
|
|
1025
1150
|
send(handler, recorder, params) if recorder
|
|
1026
1151
|
rescue StandardError => e
|
|
1027
|
-
TraceHelper.
|
|
1152
|
+
TraceHelper.warn_dropped_event("dropped a malformed BiDi #{event} event: #{e.class}")
|
|
1028
1153
|
end
|
|
1029
1154
|
|
|
1030
1155
|
def self.request_sent(recorder, params)
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1156
|
+
key = key(params)
|
|
1157
|
+
request = params['request']
|
|
1158
|
+
recorder.network_started(key,
|
|
1159
|
+
{ method: scalar(request['method']), url: scalar(request['url']), status: nil, mime_type: nil,
|
|
1034
1160
|
started_at: time(params['timestamp']), duration_ms: nil, bytes: nil, error: nil },
|
|
1035
1161
|
at: params['timestamp'])
|
|
1036
1162
|
end
|
|
1037
1163
|
|
|
1038
1164
|
def self.response_completed(recorder, params)
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1165
|
+
key = key(params)
|
|
1166
|
+
response = params['response'].is_a?(Hash) ? params['response'] : {}
|
|
1167
|
+
recorder.network_completed(key,
|
|
1168
|
+
{ status: scalar(response['status']), mime_type: scalar(response['mimeType']),
|
|
1169
|
+
bytes: scalar(response['bytesReceived'] || response['encodedDataLength']) },
|
|
1043
1170
|
finished_at(params))
|
|
1044
1171
|
end
|
|
1045
1172
|
|
|
@@ -1048,17 +1175,35 @@ module TraceHelper
|
|
|
1048
1175
|
def self.finished_at(params)
|
|
1049
1176
|
stamp = params['timestamp']
|
|
1050
1177
|
origin, response_end = params.dig('request', 'timings')&.values_at('timeOrigin', 'responseEnd')
|
|
1051
|
-
return stamp unless
|
|
1178
|
+
return stamp unless TraceHelper.finite_number?(origin) && TraceHelper.finite_number?(response_end) && response_end.positive?
|
|
1052
1179
|
|
|
1053
|
-
[stamp, origin + response_end].
|
|
1180
|
+
[stamp, origin + response_end].select { |ms| TraceHelper.finite_number?(ms) }.max
|
|
1054
1181
|
end
|
|
1055
1182
|
|
|
1056
1183
|
def self.fetch_error(recorder, params)
|
|
1057
|
-
recorder.network_completed(params
|
|
1184
|
+
recorder.network_completed(key(params), { error: scalar(params['errorText']) }, params['timestamp'])
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
# A redirect keeps the request id and counts its hops, so the id alone would merge the hops.
|
|
1188
|
+
# An event without an object request that has a string id is malformed: it raises, so deliver drops it.
|
|
1189
|
+
def self.key(params)
|
|
1190
|
+
request = params['request']
|
|
1191
|
+
raise ArgumentError, 'request is not an object with a string id' unless request.is_a?(Hash) && request['request'].is_a?(String)
|
|
1192
|
+
|
|
1193
|
+
hop = params['redirectCount']
|
|
1194
|
+
[request['request'], hop.is_a?(Integer) ? hop : 0]
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
# Only text, finite numbers, booleans and nil are kept from an event; a list or object becomes nil.
|
|
1198
|
+
def self.scalar(value)
|
|
1199
|
+
case value
|
|
1200
|
+
when String, true, false, nil then value
|
|
1201
|
+
when Numeric then value if TraceHelper.finite_number?(value)
|
|
1202
|
+
end
|
|
1058
1203
|
end
|
|
1059
1204
|
|
|
1060
1205
|
def self.log_entry(recorder, params)
|
|
1061
|
-
recorder.console_event(level: params['level'], text: params['text'], source: location(params),
|
|
1206
|
+
recorder.console_event(level: scalar(params['level']), text: scalar(params['text']), source: location(params),
|
|
1062
1207
|
at: time(params['timestamp']))
|
|
1063
1208
|
end
|
|
1064
1209
|
|
|
@@ -1070,7 +1215,7 @@ module TraceHelper
|
|
|
1070
1215
|
|
|
1071
1216
|
# BiDi timestamps are epoch milliseconds.
|
|
1072
1217
|
def self.time(milliseconds)
|
|
1073
|
-
return TraceHelper.timestamp unless
|
|
1218
|
+
return TraceHelper.timestamp unless TraceHelper.finite_number?(milliseconds)
|
|
1074
1219
|
|
|
1075
1220
|
TraceHelper.timestamp(Time.at(Rational(milliseconds.to_i, 1000)))
|
|
1076
1221
|
end
|
|
@@ -69,7 +69,7 @@ module VideoHelper
|
|
|
69
69
|
config['enabled'] != false && ENV['VIDEO'] != 'off' && !@disabled
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def recorder_for(
|
|
72
|
+
def recorder_for(driver)
|
|
73
73
|
<%- if mobile? -%>
|
|
74
74
|
return NullRecorder.new unless enabled?
|
|
75
75
|
<%- else -%>
|
|
@@ -77,9 +77,9 @@ module VideoHelper
|
|
|
77
77
|
<%- end -%>
|
|
78
78
|
|
|
79
79
|
<%- if mobile? -%>
|
|
80
|
-
AppiumRecorder.new(
|
|
80
|
+
AppiumRecorder.new(driver)
|
|
81
81
|
<%- else -%>
|
|
82
|
-
FrameRecorder.new(
|
|
82
|
+
FrameRecorder.new(driver)
|
|
83
83
|
<%- end -%>
|
|
84
84
|
rescue StandardError => e
|
|
85
85
|
disable!("could not create a recorder: #{e.message}")
|
|
@@ -186,11 +186,6 @@ module VideoHelper
|
|
|
186
186
|
# rubocop:enable Style/StderrPuts
|
|
187
187
|
<%- unless mobile? -%>
|
|
188
188
|
|
|
189
|
-
# Watir::Browser wraps the Selenium driver.
|
|
190
|
-
def selenium_driver(driver_or_browser)
|
|
191
|
-
driver_or_browser.respond_to?(:wd) ? driver_or_browser.wd : driver_or_browser
|
|
192
|
-
end
|
|
193
|
-
|
|
194
189
|
# Decided once per process, at the first listener/recorder_for call, so a run
|
|
195
190
|
# that can never get an ffmpeg warns at the start and pays no capture cost.
|
|
196
191
|
def ffmpeg_obtainable?
|