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
|
@@ -7,6 +7,7 @@ require 'tmpdir'
|
|
|
7
7
|
require 'yaml'
|
|
8
8
|
require 'zip'
|
|
9
9
|
require_relative '../../lib/generators/helper_generator'
|
|
10
|
+
require_relative '../../lib/ruby_raider/runtime'
|
|
10
11
|
require_relative '../integration/trace_format_checker'
|
|
11
12
|
|
|
12
13
|
$LOAD_PATH.unshift(File.expand_path('fixtures/video_helper_stubs', __dir__))
|
|
@@ -54,7 +55,7 @@ class FakeTraceBidi
|
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
# Renders the real trace_helper.tt for a Selenium project, loads it, and drives its
|
|
57
|
-
# Recorder the way
|
|
58
|
+
# Recorder the way the runtime reports to Raider.observer and the navigation listener does, with a
|
|
58
59
|
# driver double. The zips it writes are read back with rubyzip.
|
|
59
60
|
# rubocop:disable RSpec/MultipleMemoizedHelpers, RSpec/VerifiedDoubles, RSpec/ExpectOutput, RSpec/MultipleExpectations, RSpec/ExampleLength
|
|
60
61
|
describe 'Generated TraceHelper' do
|
|
@@ -67,22 +68,13 @@ describe 'Generated TraceHelper' do
|
|
|
67
68
|
let(:bidi) { FakeTraceBidi.new(jpeg) }
|
|
68
69
|
let(:html) { "<html><head><title>Login</title></head><body>#{'<p>row</p>' * 50}</body></html>" }
|
|
69
70
|
let(:capabilities) { double('capabilities', browser_name: 'chrome', browser_version: '131.0.6778.85') }
|
|
70
|
-
let(:window) { double('window', size: double('size', width: 1920, height: 1080)) }
|
|
71
71
|
let(:driver) do
|
|
72
72
|
double('driver', screenshot_as: png, page_source: html, current_url: 'https://example.test/login', title: 'Login',
|
|
73
|
-
capabilities: capabilities,
|
|
73
|
+
capabilities: capabilities, execute_script: 2,
|
|
74
74
|
bidi: bidi, window_handle: 'context-1')
|
|
75
75
|
end
|
|
76
|
-
let(:wait_helper) do
|
|
77
|
-
Module.new do
|
|
78
|
-
class << self
|
|
79
|
-
attr_accessor :observer
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
76
|
let(:stderr) { StringIO.new }
|
|
84
77
|
let(:warnings) { stderr.string.lines.grep(/\[trace\]/) }
|
|
85
|
-
let(:source) { { file: 'spec/login_page_spec.rb', line: 12 } }
|
|
86
78
|
let(:failed_test) { { identifier: 'spec/login_page_spec.rb[1:1]', full_name: 'Login page logs in', status: 'failed', attempt: 2, error: nil } }
|
|
87
79
|
|
|
88
80
|
def render_and_load
|
|
@@ -93,16 +85,25 @@ describe 'Generated TraceHelper' do
|
|
|
93
85
|
load path
|
|
94
86
|
end
|
|
95
87
|
|
|
88
|
+
# The inner viewport, which is what rects and the viewport screenshot are measured in.
|
|
89
|
+
def viewport_script(size)
|
|
90
|
+
allow(driver).to receive(:execute_script).with(/innerWidth/).and_return(size)
|
|
91
|
+
end
|
|
92
|
+
|
|
96
93
|
def started_recorder(**lambdas)
|
|
97
94
|
TraceHelper.recorder_for(driver, **lambdas).tap(&:start)
|
|
98
95
|
end
|
|
99
96
|
|
|
100
|
-
# What
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
# What the runtime reports to Raider.observer for one action, through Raider.report. With an element, it was found
|
|
98
|
+
# ready (action_ready). Returns the recorder's token.
|
|
99
|
+
def act(_recorder, action, selector, status: :passed, error: nil, within: [], args: nil, element: nil)
|
|
100
|
+
started = { action: action, locator: { strategy: 'id', selector: selector }, within: within, args: args }
|
|
101
|
+
token = Raider.report(:action_started) { started } # ACT_LINE
|
|
102
|
+
Raider.report(:action_ready, token) { { element: element } } if element
|
|
103
|
+
Raider.report(:action_finished, token) do
|
|
104
|
+
{ status: status, waited_ms: 95, retries: 1, error: error, element: element }
|
|
105
|
+
end
|
|
106
|
+
token
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
# What Page.capture's script answers: the inlined CSS, the sheets it could not read and the title.
|
|
@@ -117,6 +118,11 @@ describe 'Generated TraceHelper' do
|
|
|
117
118
|
allow(driver).to receive(:execute_script).with(/getBoundingClientRect/, element).and_return(info)
|
|
118
119
|
end
|
|
119
120
|
|
|
121
|
+
# The line of act that reports a start, which is where each recorded source points.
|
|
122
|
+
def act_line
|
|
123
|
+
File.foreach(__FILE__).find_index { |line| line.end_with?("# ACT_LINE\n") && line.include?('Raider.report') } + 1
|
|
124
|
+
end
|
|
125
|
+
|
|
120
126
|
def snapshot_of(path, index)
|
|
121
127
|
Zip::File.open(path) { |zip| zip.read(format('steps/%04d.html', index)) }
|
|
122
128
|
end
|
|
@@ -135,6 +141,7 @@ describe 'Generated TraceHelper' do
|
|
|
135
141
|
Dir.chdir(project_dir) { example.run }
|
|
136
142
|
ensure
|
|
137
143
|
$stderr = original_stderr
|
|
144
|
+
Raider.observer = nil
|
|
138
145
|
Object.send(:remove_const, :TraceHelper) if defined?(TraceHelper)
|
|
139
146
|
FileUtils.rm_rf(project_dir)
|
|
140
147
|
end
|
|
@@ -143,11 +150,11 @@ describe 'Generated TraceHelper' do
|
|
|
143
150
|
# Recorder tmpdirs land inside the project, which the around hook removes.
|
|
144
151
|
stub_const('ENV', ENV.to_h.merge(env).merge('TMPDIR' => File.join(project_dir, 'tmp')).compact)
|
|
145
152
|
FileUtils.mkdir_p(File.join(project_dir, 'tmp'))
|
|
146
|
-
stub_const('WaitHelper', wait_helper)
|
|
147
153
|
render_and_load
|
|
148
154
|
end
|
|
149
155
|
|
|
150
156
|
it 'writes a zip whose trace.json has format raider-trace, version 1 and the spec §1 top-level keys' do
|
|
157
|
+
viewport_script([1905, 969])
|
|
151
158
|
recorder = started_recorder
|
|
152
159
|
recorder.navigated('https://example.test/login')
|
|
153
160
|
trace = kept(recorder)
|
|
@@ -159,12 +166,49 @@ describe 'Generated TraceHelper' do
|
|
|
159
166
|
'test' => { 'identifier' => 'spec/login_page_spec.rb[1:1]', 'full_name' => 'Login page logs in',
|
|
160
167
|
'status' => 'failed', 'attempt' => 2, 'error' => nil },
|
|
161
168
|
'browser' => { 'name' => 'chrome', 'version' => '131.0.6778.85' },
|
|
162
|
-
'viewport' => { 'width' =>
|
|
169
|
+
'viewport' => { 'width' => 1905, 'height' => 969 }, 'device_pixel_ratio' => 2,
|
|
163
170
|
'capabilities' => { 'network' => true, 'console' => true, 'screenshots' => true, 'snapshots' => true },
|
|
164
171
|
'truncated' => { 'steps' => 0, 'screenshots' => 0, 'snapshots' => 0, 'network' => 0, 'console' => 0 })
|
|
165
172
|
expect(trace['finished_at'] >= trace['started_at']).to be(true)
|
|
166
173
|
end
|
|
167
174
|
|
|
175
|
+
it 'records a match_screenshot step with the given PNG as its screenshot, without taking another one' do
|
|
176
|
+
taken = 0
|
|
177
|
+
recorder = started_recorder(screenshot: ->(_driver) { [(taken += 1) && 'NAV', 'png'] })
|
|
178
|
+
recorder.navigated('https://example.test/login')
|
|
179
|
+
recorder.screenshot_compared(name: 'home', status: :failed, png: 'PNG-BYTES')
|
|
180
|
+
path = recorder.stop(keep: true, test: failed_test)
|
|
181
|
+
steps = TraceFormatChecker.read(path).first['steps']
|
|
182
|
+
|
|
183
|
+
expect(TraceFormatChecker.problems(path)).to eq([])
|
|
184
|
+
expect(steps.last).to include('kind' => 'action', 'action' => 'match_screenshot', 'name' => 'home', 'locator' => nil,
|
|
185
|
+
'status' => 'failed', 'error' => nil, 'screenshot' => 'steps/0002.png', 'snapshot' => nil)
|
|
186
|
+
expect(Zip::File.open(path) { |zip| zip.read('steps/0002.png') }).to eq('PNG-BYTES')
|
|
187
|
+
expect(taken).to eq(1)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'gives the match_screenshot step the comparison error and appends no failure step for it' do
|
|
191
|
+
recorder = started_recorder
|
|
192
|
+
error = RuntimeError.new('1 pixel differs')
|
|
193
|
+
recorder.screenshot_compared(name: 'home', status: :failed, png: 'PNG', error: error)
|
|
194
|
+
steps = kept(recorder, failed_test.merge(error: error))['steps']
|
|
195
|
+
|
|
196
|
+
expect(steps.map { |step| step.values_at('kind', 'action', 'status') }).to eq([%w[action match_screenshot failed]])
|
|
197
|
+
expect(steps.first['error']).to eq('class' => 'RuntimeError', 'message' => '1 pixel differs')
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'records a passed comparison and never fails the test when the step cannot be written' do
|
|
201
|
+
recorder = started_recorder
|
|
202
|
+
recorder.screenshot_compared(name: 'home', status: :passed, png: 'PNG')
|
|
203
|
+
expect(kept(recorder)['steps'].last).to include('action' => 'match_screenshot', 'status' => 'passed')
|
|
204
|
+
allow(recorder).to receive(:add).and_raise('boom')
|
|
205
|
+
expect { recorder.screenshot_compared(name: 'again', status: :passed, png: 'PNG') }.not_to raise_error
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'does not take match_screenshot as a media action' do
|
|
209
|
+
expect(TraceHelper::MEDIA_ACTIONS).not_to include('match_screenshot')
|
|
210
|
+
end
|
|
211
|
+
|
|
168
212
|
it 'records navigate and click steps in order with index, kind, action, locator, source, waited_ms, retries' do
|
|
169
213
|
recorder = started_recorder
|
|
170
214
|
recorder.navigated('https://example.test/login')
|
|
@@ -178,7 +222,7 @@ describe 'Generated TraceHelper' do
|
|
|
178
222
|
# No locator.label: a page object's element method has returned before the action runs, so it is never known.
|
|
179
223
|
expect(steps.last).to include('locator' => { 'strategy' => 'id', 'selector' => 'go',
|
|
180
224
|
'within' => [{ 'strategy' => 'css', 'selector' => '.form' }] },
|
|
181
|
-
'source' => { 'file' =>
|
|
225
|
+
'source' => { 'file' => __FILE__, 'line' => act_line },
|
|
182
226
|
'waited_ms' => 95, 'retries' => 1, 'status' => 'passed',
|
|
183
227
|
'screenshot' => 'steps/0002.jpg', 'snapshot' => 'steps/0002.html')
|
|
184
228
|
expect(steps.last['duration_ms']).to be_a(Integer)
|
|
@@ -206,47 +250,6 @@ describe 'Generated TraceHelper' do
|
|
|
206
250
|
expect(names).to contain_exactly('trace.json', 'steps/0001.jpg', 'steps/0001.html')
|
|
207
251
|
end
|
|
208
252
|
|
|
209
|
-
it 'records a Cucumber step as a cucumber_step group carrying its name, with the actions it ran as children' do
|
|
210
|
-
recorder = started_recorder
|
|
211
|
-
group = recorder.step_started(name: 'When I log in as admin', source: source)
|
|
212
|
-
act(recorder, :click, 'login')
|
|
213
|
-
recorder.step_finished(group, status: :passed, error: nil)
|
|
214
|
-
act(recorder, :click, 'outside')
|
|
215
|
-
steps = kept(recorder)['steps']
|
|
216
|
-
|
|
217
|
-
expect(steps.map { |step| step.values_at('index', 'parent_index', 'kind', 'action', 'status', 'name') })
|
|
218
|
-
.to eq([[1, nil, 'group', 'cucumber_step', 'passed', 'When I log in as admin'], [2, 1, 'action', 'click', 'passed', nil],
|
|
219
|
-
[3, nil, 'action', 'click', 'passed', nil]])
|
|
220
|
-
expect(steps[1..].map { |step| step.key?('name') }).to eq([false, false])
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
it 'records a failed Cucumber step with its error' do
|
|
224
|
-
recorder = started_recorder
|
|
225
|
-
group = recorder.step_started(name: 'Then I see the dashboard', source: source)
|
|
226
|
-
recorder.step_finished(group, status: :failed, error: RuntimeError.new('no dashboard'))
|
|
227
|
-
step = kept(recorder)['steps'].first
|
|
228
|
-
|
|
229
|
-
expect(step).to include('status' => 'failed', 'error' => { 'class' => 'RuntimeError', 'message' => 'no dashboard' })
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
it 'keeps only the last pass of eventually as child steps and counts the earlier passes in retries' do
|
|
233
|
-
recorder = started_recorder
|
|
234
|
-
group = recorder.group_started(action: :eventually, source: source)
|
|
235
|
-
2.times do |pass|
|
|
236
|
-
act(recorder, :click, 'refresh')
|
|
237
|
-
act(recorder, :text, 'welcome', status: :failed, error: RuntimeError.new("not yet #{pass}"))
|
|
238
|
-
recorder.group_retried(group, error: RuntimeError.new("not yet #{pass}"))
|
|
239
|
-
end
|
|
240
|
-
act(recorder, :click, 'refresh')
|
|
241
|
-
act(recorder, :text, 'welcome')
|
|
242
|
-
recorder.group_finished(group, status: :passed, retries: 2, error: nil)
|
|
243
|
-
steps = kept(recorder)['steps']
|
|
244
|
-
|
|
245
|
-
expect(steps.map { |step| step.values_at('index', 'parent_index', 'kind', 'action', 'status') })
|
|
246
|
-
.to eq([[1, nil, 'group', 'eventually', 'passed'], [2, 1, 'action', 'click', 'passed'], [3, 1, 'action', 'text', 'passed']])
|
|
247
|
-
expect(steps.first).to include('retries' => 2, 'error' => { 'class' => 'RuntimeError', 'message' => 'not yet 1' }, 'screenshot' => nil, 'snapshot' => nil)
|
|
248
|
-
end
|
|
249
|
-
|
|
250
253
|
it 'appends a failure step with the error when no traced step was running' do
|
|
251
254
|
recorder = started_recorder
|
|
252
255
|
recorder.navigated('https://example.test/login')
|
|
@@ -278,20 +281,6 @@ describe 'Generated TraceHelper' do
|
|
|
278
281
|
.to eq([['action', 'failed', { 'class' => 'RuntimeError', 'message' => 'Waited 10s for {id: "go"} to be visible' }]])
|
|
279
282
|
end
|
|
280
283
|
|
|
281
|
-
it 'still gives the test error a step when the step that raised it was dropped by eventually' do
|
|
282
|
-
recorder = started_recorder
|
|
283
|
-
error = RuntimeError.new('not yet')
|
|
284
|
-
group = recorder.group_started(action: :eventually, source: source)
|
|
285
|
-
act(recorder, :click, 'refresh', status: :failed, error: error)
|
|
286
|
-
recorder.group_retried(group, error: error)
|
|
287
|
-
act(recorder, :click, 'refresh')
|
|
288
|
-
recorder.group_finished(group, status: :passed, retries: 1, error: nil)
|
|
289
|
-
steps = kept(recorder, failed_test.merge(error: error))['steps']
|
|
290
|
-
|
|
291
|
-
expect(steps.map { |step| step.values_at('kind', 'status') }).to eq([%w[group passed], %w[action passed]])
|
|
292
|
-
expect(steps.first['error']).to eq('class' => 'RuntimeError', 'message' => 'not yet')
|
|
293
|
-
end
|
|
294
|
-
|
|
295
284
|
it 'appends a failure step when the step that raised the test error was dropped by max_steps' do
|
|
296
285
|
allow(TraceHelper).to receive(:config).and_return(trace_config.merge('max_steps' => 2))
|
|
297
286
|
recorder = started_recorder
|
|
@@ -317,6 +306,20 @@ describe 'Generated TraceHelper' do
|
|
|
317
306
|
expect(TraceHelper.enabled?).to be(true)
|
|
318
307
|
end
|
|
319
308
|
|
|
309
|
+
describe 'viewport' do
|
|
310
|
+
it 'is nil when the inner size cannot be read' do
|
|
311
|
+
viewport_script(nil)
|
|
312
|
+
|
|
313
|
+
expect(kept(started_recorder)['viewport']).to eq('width' => nil, 'height' => nil)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it 'is nil when the script raises' do
|
|
317
|
+
allow(driver).to receive(:execute_script).with(/innerWidth/).and_raise(Selenium::WebDriver::Error::WebDriverError, 'no page')
|
|
318
|
+
|
|
319
|
+
expect(kept(started_recorder)['viewport']).to eq('width' => nil, 'height' => nil)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
320
323
|
describe 'snapshots' do
|
|
321
324
|
def sanitize(page, css: '', skipped: 0)
|
|
322
325
|
TraceHelper::Snapshot.sanitize(page, css: css, skipped: skipped)
|
|
@@ -458,11 +461,10 @@ describe 'Generated TraceHelper' do
|
|
|
458
461
|
element_script(field, secret: false)
|
|
459
462
|
recorder = started_recorder
|
|
460
463
|
act(recorder, :send_keys, 'user', args: ['admin', :enter], element: field)
|
|
461
|
-
act(recorder, :select_by, 'colour', args: [:text, 'Blue'], element: field)
|
|
462
464
|
act(recorder, :click, 'go', element: field)
|
|
463
465
|
steps = kept(recorder)['steps']
|
|
464
466
|
|
|
465
|
-
expect(steps.map { |step| step.values_at('value', 'secret') }).to eq([['admin{enter}', false], [
|
|
467
|
+
expect(steps.map { |step| step.values_at('value', 'secret') }).to eq([['admin{enter}', false], [nil, nil]])
|
|
466
468
|
expect(steps.last.keys).not_to include('value', 'secret')
|
|
467
469
|
end
|
|
468
470
|
|
|
@@ -578,17 +580,25 @@ describe 'Generated TraceHelper' do
|
|
|
578
580
|
expect(Dir.children(File.join(project_dir, 'tmp'))).to be_empty
|
|
579
581
|
end
|
|
580
582
|
|
|
581
|
-
it 'makes the recorder the
|
|
583
|
+
it 'makes the recorder the Raider observer until cleanup, so no test inherits it' do
|
|
582
584
|
recorder = started_recorder
|
|
583
|
-
expect(
|
|
585
|
+
expect(Raider.observer).to be(recorder)
|
|
584
586
|
|
|
585
587
|
recorder.stop(keep: false, test: failed_test)
|
|
586
588
|
recorder.cleanup
|
|
587
|
-
expect(
|
|
589
|
+
expect(Raider.observer).to be_nil
|
|
588
590
|
end
|
|
589
591
|
|
|
590
|
-
it '
|
|
591
|
-
|
|
592
|
+
it 'leaves an observer that is not the recorder alone on release' do
|
|
593
|
+
other = Object.new
|
|
594
|
+
recorder = started_recorder
|
|
595
|
+
Raider.observer = other
|
|
596
|
+
TraceHelper.release(recorder)
|
|
597
|
+
|
|
598
|
+
expect(Raider.observer).to be(other)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
it 'records navigation and the failure with no observer events' do
|
|
592
602
|
recorder = started_recorder
|
|
593
603
|
recorder.navigated('https://example.test/login')
|
|
594
604
|
steps = kept(recorder, failed_test.merge(error: RuntimeError.new('boom')))['steps']
|
|
@@ -596,25 +606,50 @@ describe 'Generated TraceHelper' do
|
|
|
596
606
|
expect(steps.map { |step| step['kind'] }).to eq(%w[navigate failure])
|
|
597
607
|
end
|
|
598
608
|
|
|
609
|
+
it 'takes each step source from the first frame outside the runtime and the helpers' do
|
|
610
|
+
recorder = started_recorder
|
|
611
|
+
act(recorder, :click, 'go')
|
|
612
|
+
act(recorder, :send_keys, 'user', args: ['admin'])
|
|
613
|
+
sources = kept(recorder)['steps'].map { |step| step['source'] }
|
|
614
|
+
|
|
615
|
+
expect(sources).to all(include('file' => __FILE__))
|
|
616
|
+
expect(sources.first['line']).to be_a(Integer)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
it 'gives the token nil, so the runtime sends no ready or finished, when not recording' do
|
|
620
|
+
recorder = TraceHelper.recorder_for(driver)
|
|
621
|
+
|
|
622
|
+
expect(recorder.action_started(action: :click, locator: { strategy: 'id', selector: 'go' }, within: [])).to be_nil
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
it 'records the actions the runtime emits as steps with their locator strategy' do
|
|
626
|
+
recorder = started_recorder
|
|
627
|
+
%w[click send_keys clear submit].each { |action| act(recorder, action, 'go', args: nil) }
|
|
628
|
+
steps = kept(recorder)['steps']
|
|
629
|
+
|
|
630
|
+
expect(steps.map { |step| step['action'] }).to eq(%w[click send_keys clear submit])
|
|
631
|
+
expect(TraceHelper::MEDIA_ACTIONS).to eq(%w[navigate click send_keys clear submit])
|
|
632
|
+
end
|
|
633
|
+
|
|
599
634
|
context 'with TRACE=off' do
|
|
600
635
|
let(:env) { { 'TRACE' => 'off' } }
|
|
601
636
|
|
|
602
|
-
it 'returns a NullRecorder and leaves
|
|
637
|
+
it 'returns a NullRecorder and leaves Raider.observer nil with TRACE=off or enabled: false' do
|
|
603
638
|
recorder = TraceHelper.recorder_for(driver)
|
|
604
639
|
|
|
605
640
|
expect(recorder).to be_a(TraceHelper::NullRecorder)
|
|
606
641
|
expect(recorder.start).to be_nil
|
|
607
642
|
expect(recorder.stop(keep: true, test: failed_test)).to be_nil
|
|
608
|
-
expect(
|
|
643
|
+
expect(Raider.observer).to be_nil
|
|
609
644
|
end
|
|
610
645
|
end
|
|
611
646
|
|
|
612
647
|
context 'with enabled: false' do
|
|
613
648
|
let(:trace_config) { { 'enabled' => false } }
|
|
614
649
|
|
|
615
|
-
it 'returns a NullRecorder and leaves
|
|
650
|
+
it 'returns a NullRecorder and leaves Raider.observer nil with TRACE=off or enabled: false' do
|
|
616
651
|
expect(TraceHelper.recorder_for(driver)).to be_a(TraceHelper::NullRecorder)
|
|
617
|
-
expect(
|
|
652
|
+
expect(Raider.observer).to be_nil
|
|
618
653
|
end
|
|
619
654
|
end
|
|
620
655
|
|
|
@@ -681,18 +716,214 @@ describe 'Generated TraceHelper' do
|
|
|
681
716
|
expect(warnings).to eq(["[trace] could not write the trace: No space left on device; tracing disabled for this run\n"])
|
|
682
717
|
expect(TraceHelper.enabled?).to be(false)
|
|
683
718
|
expect(TraceHelper.recorder_for(driver)).to be_a(TraceHelper::NullRecorder)
|
|
684
|
-
expect(
|
|
719
|
+
expect(Raider.observer).to be_nil
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
describe 'a navigation in flight' do
|
|
723
|
+
let(:started_ms) { 1_793_568_600_020 }
|
|
724
|
+
|
|
725
|
+
def request_sent(id, url)
|
|
726
|
+
bidi.emit('network.beforeRequestSent', 'request' => { 'request' => id, 'method' => 'GET', 'url' => url },
|
|
727
|
+
'timestamp' => started_ms)
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
it 'credits requests made between navigating and navigated to the navigate step' do
|
|
731
|
+
recorder = started_recorder
|
|
732
|
+
recorder.navigating('https://example.test/start')
|
|
733
|
+
request_sent('r1', 'https://example.test/start')
|
|
734
|
+
recorder.navigated('https://example.test/')
|
|
735
|
+
act(recorder, :click, 'go')
|
|
736
|
+
request_sent('r2', 'https://example.test/api')
|
|
737
|
+
trace = kept(recorder)
|
|
738
|
+
|
|
739
|
+
expect(trace['steps'].map { |step| step.values_at('index', 'action', 'status') }).to eq([[1, 'navigate', 'passed'], [2, 'click', 'passed']])
|
|
740
|
+
expect(trace['network'].map { |entry| entry.values_at('step_index', 'url') })
|
|
741
|
+
.to eq([[1, 'https://example.test/start'], [2, 'https://example.test/api']])
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
it 'keeps the old path: navigated alone adds one finished navigate step' do
|
|
745
|
+
recorder = started_recorder
|
|
746
|
+
recorder.navigated('https://example.test/login')
|
|
747
|
+
|
|
748
|
+
expect(kept(recorder)['steps'].map { |step| step.values_at('kind', 'status') }).to eq([%w[navigate passed]])
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
it 'finishes a navigation that never completed as failed at the next navigating' do
|
|
752
|
+
recorder = started_recorder
|
|
753
|
+
recorder.navigating('https://example.test/down')
|
|
754
|
+
recorder.navigating('https://example.test/up')
|
|
755
|
+
recorder.navigated('https://example.test/up')
|
|
756
|
+
steps = kept(recorder)['steps']
|
|
757
|
+
|
|
758
|
+
expect(steps.map { |step| step.values_at('kind', 'status', 'url') })
|
|
759
|
+
.to eq([['navigate', 'failed', 'https://example.test/down'], ['navigate', 'passed', 'https://example.test/login']])
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
it 'finishes a navigation that never completed as failed at stop' do
|
|
763
|
+
recorder = started_recorder
|
|
764
|
+
recorder.navigating('https://example.test/down')
|
|
765
|
+
steps = kept(recorder)['steps']
|
|
766
|
+
|
|
767
|
+
expect(steps.map { |step| step.values_at('kind', 'status') }).to eq([%w[navigate failed]])
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
it 'finishes a navigation that never completed as failed when the next action is added, with only its own duration' do
|
|
771
|
+
clock = 100.0
|
|
772
|
+
allow(TraceHelper).to receive(:monotonic_now) { clock }
|
|
773
|
+
recorder = started_recorder
|
|
774
|
+
recorder.navigating('https://example.test/down')
|
|
775
|
+
clock += 0.25
|
|
776
|
+
act(recorder, :click, 'go')
|
|
777
|
+
clock += 60
|
|
778
|
+
steps = kept(recorder)['steps']
|
|
779
|
+
|
|
780
|
+
expect(steps.map { |step| step.values_at('action', 'status') }).to eq([%w[navigate failed], %w[click passed]])
|
|
781
|
+
expect(steps.first['duration_ms']).to eq(250)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
it 'adds no second failure step for the error a pending navigation ends the test with' do
|
|
785
|
+
error = Selenium::WebDriver::Error::WebDriverError.new('net::ERR_CONNECTION_REFUSED')
|
|
786
|
+
recorder = started_recorder
|
|
787
|
+
recorder.navigating('https://example.test/down')
|
|
788
|
+
steps = kept(recorder, failed_test.merge(error: error))['steps']
|
|
789
|
+
|
|
790
|
+
expect(steps.map { |step| step.values_at('kind', 'status') }).to eq([%w[navigate failed]])
|
|
791
|
+
expect(steps.first['error']).to include('message' => a_string_including('ERR_CONNECTION_REFUSED'))
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
it 'adds no failure step for the error of the action that followed a pending navigation' do
|
|
795
|
+
error = RuntimeError.new('boom')
|
|
796
|
+
recorder = started_recorder
|
|
797
|
+
recorder.navigating('https://example.test/down')
|
|
798
|
+
act(recorder, :click, 'go', status: :failed, error: error)
|
|
799
|
+
steps = kept(recorder, failed_test.merge(error: error))['steps']
|
|
800
|
+
|
|
801
|
+
expect(steps.map { |step| step.values_at('kind', 'status') }).to eq([%w[navigate failed], %w[action failed]])
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
it 'keeps the page url a back or forward navigation started from when the driver does not give one' do
|
|
805
|
+
alert = { open: false }
|
|
806
|
+
stub_const('VideoHelper', Module.new { define_singleton_method(:alert_open?) { |_driver| alert[:open] } })
|
|
807
|
+
recorder = started_recorder
|
|
808
|
+
recorder.navigated('https://example.test/login')
|
|
809
|
+
alert[:open] = true
|
|
810
|
+
recorder.navigating(nil)
|
|
811
|
+
recorder.navigated(nil)
|
|
812
|
+
steps = kept(recorder)['steps']
|
|
813
|
+
|
|
814
|
+
expect(steps.map { |step| step['url'] }).to eq(['https://example.test/login', 'https://example.test/login'])
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
it 'is driven by the listener before each navigation, and the null recorder ignores it' do
|
|
818
|
+
listener = TraceHelper.listener
|
|
819
|
+
recorder = started_recorder
|
|
820
|
+
listener.before_navigate_to('https://example.test/a', driver)
|
|
821
|
+
listener.after_navigate_to('https://example.test/a', driver)
|
|
822
|
+
listener.before_navigate_back(driver)
|
|
823
|
+
listener.after_navigate_back(driver)
|
|
824
|
+
listener.before_navigate_forward(driver)
|
|
825
|
+
listener.after_navigate_forward(driver)
|
|
826
|
+
|
|
827
|
+
expect(kept(recorder)['steps'].map { |step| step['status'] }).to eq(%w[passed passed passed])
|
|
828
|
+
expect(TraceHelper::NullRecorder.new.navigating('https://example.test/')).to be_nil
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
describe 'mocked requests' do
|
|
833
|
+
let(:started_ms) { 1_793_568_600_020 }
|
|
834
|
+
let(:recorder) { started_recorder }
|
|
835
|
+
let(:network) { kept(recorder)['network'] }
|
|
836
|
+
|
|
837
|
+
before { recorder.navigated('https://example.test/login') }
|
|
838
|
+
|
|
839
|
+
def start(id, hop = 0)
|
|
840
|
+
recorder.network_started([id, hop], { method: 'GET', url: "https://example.test/#{id}", status: nil, duration_ms: nil }, at: started_ms)
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def complete(id, status, hop = 0)
|
|
844
|
+
recorder.network_completed([id, hop], { status: status }, started_ms + 7)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
it 'marks a pending entry and still completes it' do
|
|
848
|
+
start('r1')
|
|
849
|
+
recorder.network_marked(['r1', 0], { mocked: 'fulfilled' })
|
|
850
|
+
complete('r1', 200)
|
|
851
|
+
|
|
852
|
+
expect(network.map { |entry| entry.values_at('mocked', 'status', 'duration_ms') }).to eq([['fulfilled', 200, 7]])
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
it 'applies an early mark when the entry starts' do
|
|
856
|
+
recorder.network_marked(['r1', 0], { mocked: 'failed' })
|
|
857
|
+
start('r1')
|
|
858
|
+
|
|
859
|
+
expect(network.map { |entry| entry['mocked'] }).to eq(['failed'])
|
|
860
|
+
expect(recorder.instance_variable_get(:@early_marks)).to be_empty
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
it 'marks only its hop' do
|
|
864
|
+
start('r1', 0)
|
|
865
|
+
start('r1', 1)
|
|
866
|
+
recorder.network_marked(['r1', 1], { mocked: 'fulfilled' })
|
|
867
|
+
|
|
868
|
+
expect(network.map { |entry| entry['mocked'] }).to eq([nil, 'fulfilled'])
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
it 'caps early marks at MAX_PENDING' do
|
|
872
|
+
cap = TraceHelper::Recorder::MAX_PENDING
|
|
873
|
+
(cap + 5).times { |index| recorder.network_marked(["m#{index}", 0], { mocked: 'fulfilled' }) }
|
|
874
|
+
marks = recorder.instance_variable_get(:@early_marks)
|
|
875
|
+
|
|
876
|
+
expect(marks.size).to eq(cap)
|
|
877
|
+
expect(marks.keys.first).to eq(['m5', 0])
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
it 'keeps the status from the mark when no completion arrives' do
|
|
881
|
+
start('r1')
|
|
882
|
+
recorder.network_marked(['r1', 0], { mocked: 'fulfilled', status: 503 })
|
|
883
|
+
|
|
884
|
+
expect(network.map { |entry| entry.values_at('mocked', 'status') }).to eq([['fulfilled', 503]])
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
it 'keeps the mark status when the completion has none' do
|
|
888
|
+
start('r1')
|
|
889
|
+
recorder.network_marked(['r1', 0], { mocked: 'fulfilled', status: 503 })
|
|
890
|
+
complete('r1', nil)
|
|
891
|
+
|
|
892
|
+
expect(network.map { |entry| entry['status'] }).to eq([503])
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
it 'is fed by NetworkHelper.observer, registered when recorder_for runs' do
|
|
896
|
+
helper = Module.new { class << self; attr_accessor :observer; end }
|
|
897
|
+
stub_const('NetworkHelper', helper)
|
|
898
|
+
started = started_recorder
|
|
899
|
+
started.network_started(['r1', 0], { url: 'https://example.test/r1' })
|
|
900
|
+
helper.observer.request_mocked(request_id: 'r1', redirect_count: 0, outcome: 'failed', status: nil)
|
|
901
|
+
|
|
902
|
+
expect(helper.observer).to be(TraceHelper::MockMarks)
|
|
903
|
+
expect(kept(started)['network'].map { |entry| entry['mocked'] }).to eq(['failed'])
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
it 'does not raise when a mark cannot be applied' do
|
|
907
|
+
allow(TraceHelper).to receive(:active_recorder).and_raise(RuntimeError, 'boom')
|
|
908
|
+
|
|
909
|
+
expect { TraceHelper::MockMarks.request_mocked(request_id: 'r1', redirect_count: 0, outcome: 'failed') }.not_to raise_error
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
it 'is accepted by the format checker, which rejects other values' do
|
|
913
|
+
trace = { 'network' => [{ 'mocked' => 'maybe' }, { 'mocked' => 'fulfilled' }, {}] }
|
|
914
|
+
expect(TraceFormatChecker.mocked_rules(trace['network']).size).to eq(1)
|
|
915
|
+
end
|
|
685
916
|
end
|
|
686
917
|
|
|
687
918
|
describe 'browser wiring' do
|
|
688
919
|
let(:started_ms) { 1_793_568_600_020 }
|
|
689
920
|
let(:bidi_events) { %w[network.beforeRequestSent network.responseCompleted network.fetchError log.entryAdded] }
|
|
690
921
|
|
|
691
|
-
def request_sent(id, method, url, at)
|
|
692
|
-
bidi.emit('network.beforeRequestSent',
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
922
|
+
def request_sent(id, method, url, at, hop: 0)
|
|
923
|
+
bidi.emit('network.beforeRequestSent', 'redirectCount' => hop,
|
|
924
|
+
'request' => { 'request' => id, 'method' => method, 'url' => url,
|
|
925
|
+
'headers' => [{ 'name' => 'Cookie', 'value' => { 'type' => 'string', 'value' => 'session=hush' } }] },
|
|
926
|
+
'timestamp' => at)
|
|
696
927
|
end
|
|
697
928
|
|
|
698
929
|
it 'subscribes passively to the four BiDi events and never adds a request handler' do
|
|
@@ -780,6 +1011,151 @@ describe 'Generated TraceHelper' do
|
|
|
780
1011
|
)
|
|
781
1012
|
end
|
|
782
1013
|
|
|
1014
|
+
# A redirect keeps the request id; each hop has its own redirectCount. Chrome starts hop 1 in the same
|
|
1015
|
+
# millisecond hop 0 completes, so the two events can arrive in either order.
|
|
1016
|
+
it 'keeps each hop of a redirect as its own entry with its own status and duration' do
|
|
1017
|
+
recorder = started_recorder
|
|
1018
|
+
recorder.navigated('https://example.test/start')
|
|
1019
|
+
request_sent('r1', 'GET', 'https://example.test/start', started_ms)
|
|
1020
|
+
request_sent('r1', 'GET', 'https://example.test/', started_ms + 1, hop: 1)
|
|
1021
|
+
bidi.emit('network.responseCompleted', 'redirectCount' => 0, 'timestamp' => started_ms + 1,
|
|
1022
|
+
'request' => { 'request' => 'r1' }, 'response' => { 'status' => 302 })
|
|
1023
|
+
bidi.emit('network.responseCompleted', 'redirectCount' => 1, 'timestamp' => started_ms + 206,
|
|
1024
|
+
'request' => { 'request' => 'r1' }, 'response' => { 'status' => 200 })
|
|
1025
|
+
|
|
1026
|
+
expect(kept(recorder)['network'].map { |entry| entry.values_at('url', 'status', 'duration_ms') }).to eq(
|
|
1027
|
+
[['https://example.test/start', 302, 1], ['https://example.test/', 200, 205]]
|
|
1028
|
+
)
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
it 'drops one malformed BiDi event, warns once and keeps tracing' do
|
|
1032
|
+
recorder = started_recorder
|
|
1033
|
+
bidi.emit('network.beforeRequestSent', 'request' => 'string', 'timestamp' => started_ms)
|
|
1034
|
+
bidi.emit('network.responseCompleted', 'request' => { 'request' => 'bad', 'timings' => 'string' },
|
|
1035
|
+
'response' => 'string', 'timestamp' => started_ms)
|
|
1036
|
+
bidi.emit('log.entryAdded', 'level' => 'info', 'text' => 'bad', 'timestamp' => started_ms, 'stackTrace' => 'string')
|
|
1037
|
+
request_sent('r1', 'GET', 'https://example.test/ok', started_ms)
|
|
1038
|
+
bidi.emit('network.responseCompleted', 'request' => { 'request' => 'r1' }, 'timestamp' => started_ms + 5,
|
|
1039
|
+
'response' => { 'status' => 200 })
|
|
1040
|
+
bidi.emit('log.entryAdded', 'level' => 'info', 'text' => 'good', 'timestamp' => started_ms)
|
|
1041
|
+
trace = kept(recorder)
|
|
1042
|
+
|
|
1043
|
+
expect(trace['network'].map { |entry| [entry['url'], entry['status']] }).to include(['https://example.test/ok', 200])
|
|
1044
|
+
expect(trace['console'].map { |entry| entry['text'] }).to include('good')
|
|
1045
|
+
expect(TraceHelper.enabled?).to be(true)
|
|
1046
|
+
expect(warnings.size).to eq(1)
|
|
1047
|
+
expect(warnings.first).to match(/dropped a malformed BiDi .* event/)
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
describe 'hardening against hostile event content' do
|
|
1051
|
+
it 'keeps tracing and records no duration for a timing or timestamp that is not finite' do
|
|
1052
|
+
recorder = started_recorder
|
|
1053
|
+
recorder.navigated('https://example.test/login')
|
|
1054
|
+
request_sent('r1', 'GET', 'https://example.test/a', started_ms)
|
|
1055
|
+
bidi.emit('network.responseCompleted', 'request' => { 'request' => 'r1' }, 'timestamp' => Float::INFINITY,
|
|
1056
|
+
'response' => { 'status' => 200 })
|
|
1057
|
+
request_sent('r2', 'GET', 'https://example.test/b', Float::INFINITY)
|
|
1058
|
+
bidi.emit('network.responseCompleted', 'request' => { 'request' => 'r2', 'timings' => { 'timeOrigin' => Float::INFINITY, 'responseEnd' => 5 } },
|
|
1059
|
+
'timestamp' => started_ms, 'response' => { 'status' => 200 })
|
|
1060
|
+
bidi.emit('log.entryAdded', 'level' => 'info', 'text' => 'inf', 'timestamp' => Float::INFINITY)
|
|
1061
|
+
trace = kept(recorder)
|
|
1062
|
+
|
|
1063
|
+
expect(TraceHelper.enabled?).to be(true)
|
|
1064
|
+
expect(trace['network'].map { |entry| entry.values_at('url', 'status', 'duration_ms') })
|
|
1065
|
+
.to eq([['https://example.test/a', 200, nil], ['https://example.test/b', 200, nil]])
|
|
1066
|
+
expect(trace['console'].map { |entry| entry['text'] }).to eq(['inf'])
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
it 'drops, and records nothing for, an event whose request is not an object with a string id' do
|
|
1070
|
+
recorder = started_recorder
|
|
1071
|
+
recorder.navigated('https://example.test/login')
|
|
1072
|
+
bidi.emit('network.beforeRequestSent', 'request' => 'string', 'timestamp' => started_ms)
|
|
1073
|
+
bidi.emit('network.beforeRequestSent', 'request' => { 'request' => 123, 'url' => 'https://example.test/x' }, 'timestamp' => started_ms)
|
|
1074
|
+
bidi.emit('network.responseCompleted', 'request' => 'string', 'timestamp' => started_ms, 'response' => { 'status' => 200 })
|
|
1075
|
+
bidi.emit('network.responseCompleted', 'request' => { 'request' => 123 }, 'timestamp' => started_ms, 'response' => { 'status' => 200 })
|
|
1076
|
+
bidi.emit('network.fetchError', 'request' => 'string', 'errorText' => 'x', 'timestamp' => started_ms)
|
|
1077
|
+
bidi.emit('network.fetchError', 'request' => { 'request' => 123 }, 'errorText' => 'x', 'timestamp' => started_ms)
|
|
1078
|
+
pending = recorder.instance_variable_get(:@requests).size + recorder.instance_variable_get(:@early).size
|
|
1079
|
+
|
|
1080
|
+
expect(pending).to eq(0)
|
|
1081
|
+
expect(kept(recorder)['network']).to eq([])
|
|
1082
|
+
expect(TraceHelper.enabled?).to be(true)
|
|
1083
|
+
expect(warnings.size).to eq(1)
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
it 'treats a redirectCount that is not an Integer as 0' do
|
|
1087
|
+
recorder = started_recorder
|
|
1088
|
+
recorder.navigated('https://example.test/login')
|
|
1089
|
+
request_sent('r1', 'GET', 'https://example.test/a', started_ms, hop: '7')
|
|
1090
|
+
bidi.emit('network.responseCompleted', 'redirectCount' => 0, 'request' => { 'request' => 'r1' }, 'timestamp' => started_ms + 4,
|
|
1091
|
+
'response' => { 'status' => 200 })
|
|
1092
|
+
|
|
1093
|
+
expect(kept(recorder)['network'].map { |entry| entry.values_at('status', 'duration_ms') }).to eq([[200, 4]])
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
it 'keeps only scalar values for network and console fields' do
|
|
1097
|
+
recorder = started_recorder
|
|
1098
|
+
recorder.navigated('https://example.test/login')
|
|
1099
|
+
bidi.emit('network.beforeRequestSent', 'request' => { 'request' => 'r1', 'method' => ['GET'], 'url' => { 'nested' => 'x' } },
|
|
1100
|
+
'timestamp' => started_ms)
|
|
1101
|
+
bidi.emit('network.responseCompleted', 'request' => { 'request' => 'r1' }, 'timestamp' => started_ms + 1,
|
|
1102
|
+
'response' => { 'status' => { 'a' => 1 }, 'mimeType' => ['x'], 'bytesReceived' => { 'b' => 2 } })
|
|
1103
|
+
bidi.emit('network.beforeRequestSent', 'request' => { 'request' => 'r3', 'method' => 'GET', 'url' => 'https://example.test/n' },
|
|
1104
|
+
'timestamp' => started_ms)
|
|
1105
|
+
bidi.emit('network.responseCompleted', 'request' => { 'request' => 'r3' }, 'timestamp' => started_ms + 1,
|
|
1106
|
+
'response' => { 'status' => 200, 'bytesReceived' => Float::INFINITY })
|
|
1107
|
+
bidi.emit('network.beforeRequestSent', 'request' => { 'request' => 'r2', 'method' => 'GET', 'url' => 'https://example.test/e' },
|
|
1108
|
+
'timestamp' => started_ms)
|
|
1109
|
+
bidi.emit('network.fetchError', 'request' => { 'request' => 'r2' }, 'errorText' => { 'deep' => 'x' }, 'timestamp' => started_ms + 1)
|
|
1110
|
+
bidi.emit('log.entryAdded', 'level' => { 'a' => 1 }, 'text' => %w[a b], 'timestamp' => started_ms)
|
|
1111
|
+
trace = kept(recorder)
|
|
1112
|
+
|
|
1113
|
+
expect(trace['network'].map { |entry| entry.values_at('method', 'url', 'status', 'mime_type', 'bytes', 'error') })
|
|
1114
|
+
.to eq([[nil, nil, nil, nil, nil, nil], ['GET', 'https://example.test/n', 200, nil, nil, nil],
|
|
1115
|
+
['GET', 'https://example.test/e', nil, nil, nil, nil]])
|
|
1116
|
+
expect(trace['console'].map { |entry| entry.values_at('level', 'text') }).to eq([[nil, nil]])
|
|
1117
|
+
end
|
|
1118
|
+
|
|
1119
|
+
it 'caps the pending requests and the early completions, dropping the oldest' do
|
|
1120
|
+
recorder = started_recorder
|
|
1121
|
+
cap = TraceHelper::Recorder::MAX_PENDING
|
|
1122
|
+
(cap + 5).times do |index|
|
|
1123
|
+
recorder.network_started("s#{index}", { url: "https://example.test/#{index}" })
|
|
1124
|
+
recorder.network_completed("e#{index}", { status: 200 })
|
|
1125
|
+
end
|
|
1126
|
+
requests = recorder.instance_variable_get(:@requests)
|
|
1127
|
+
early = recorder.instance_variable_get(:@early)
|
|
1128
|
+
|
|
1129
|
+
expect([requests.size, early.size]).to eq([cap, cap])
|
|
1130
|
+
expect(requests.keys.first).to eq('s5')
|
|
1131
|
+
expect(early.keys.first).to eq('e5')
|
|
1132
|
+
expect(TraceHelper.enabled?).to be(true)
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
it 'warns about a dropped event even after another notice, and the notice is not hidden by it' do
|
|
1136
|
+
TraceHelper.warn_once('something left out')
|
|
1137
|
+
recorder = started_recorder
|
|
1138
|
+
bidi.emit('network.beforeRequestSent', 'request' => 'string', 'timestamp' => started_ms)
|
|
1139
|
+
bidi.emit('network.beforeRequestSent', 'request' => 'string', 'timestamp' => started_ms)
|
|
1140
|
+
TraceHelper.warn_once('another')
|
|
1141
|
+
|
|
1142
|
+
expect(warnings.size).to eq(2)
|
|
1143
|
+
expect(warnings.first).to eq("[trace] something left out\n")
|
|
1144
|
+
expect(warnings.last).to match(/dropped a malformed BiDi network.beforeRequestSent event/)
|
|
1145
|
+
expect(recorder).not_to be_nil
|
|
1146
|
+
expect(TraceHelper.enabled?).to be(true)
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
it 'warns once about a dropped event before any other notice, leaving that notice free' do
|
|
1150
|
+
started_recorder
|
|
1151
|
+
bidi.emit('log.entryAdded', 'level' => 'info', 'text' => 'x', 'timestamp' => 1, 'stackTrace' => 'string')
|
|
1152
|
+
TraceHelper.warn_once('something left out')
|
|
1153
|
+
|
|
1154
|
+
expect(warnings.size).to eq(2)
|
|
1155
|
+
expect(warnings.last).to eq("[trace] something left out\n")
|
|
1156
|
+
end
|
|
1157
|
+
end
|
|
1158
|
+
|
|
783
1159
|
it 'records capabilities network/console false and warns once when subscribe raises' do
|
|
784
1160
|
bidi.failure = Selenium::WebDriver::Error::WebDriverError.new('no such frame')
|
|
785
1161
|
traces = Array.new(2) do
|
|
@@ -846,6 +1222,14 @@ describe 'Generated TraceHelper' do
|
|
|
846
1222
|
.to eq([true, true, true, true, false, false])
|
|
847
1223
|
end
|
|
848
1224
|
|
|
1225
|
+
context 'with BIDI=off' do
|
|
1226
|
+
let(:env) { { 'BIDI' => 'off' } }
|
|
1227
|
+
|
|
1228
|
+
it 'is false for every browser' do
|
|
1229
|
+
expect(TraceHelper.bidi?('chrome')).to be(false)
|
|
1230
|
+
end
|
|
1231
|
+
end
|
|
1232
|
+
|
|
849
1233
|
context 'with TRACE=off' do
|
|
850
1234
|
let(:env) { { 'TRACE' => 'off' } }
|
|
851
1235
|
|
|
@@ -872,7 +1256,7 @@ describe 'Generated TraceHelper' do
|
|
|
872
1256
|
|
|
873
1257
|
def load_driver_helper
|
|
874
1258
|
generator = HelpersGenerator.new(%w[selenium rspec proj])
|
|
875
|
-
%w[driver_helper video_helper].each do |name|
|
|
1259
|
+
%w[driver_helper video_helper network_helper screenshot_helper].each do |name|
|
|
876
1260
|
source = File.read(File.expand_path("../../lib/generators/templates/helpers/#{name}.tt", __dir__))
|
|
877
1261
|
File.write(File.join(project_dir, "#{name}.rb"), ERB.new(source, trim_mode: '-').result(generator.instance_eval { binding }))
|
|
878
1262
|
end
|
|
@@ -906,7 +1290,7 @@ describe 'Generated TraceHelper' do
|
|
|
906
1290
|
before { load_driver_helper }
|
|
907
1291
|
|
|
908
1292
|
after do
|
|
909
|
-
%i[DriverHelper VideoHelper].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) }
|
|
1293
|
+
%i[DriverHelper VideoHelper NetworkHelper].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) }
|
|
910
1294
|
$LOADED_FEATURES.delete(trace_feature)
|
|
911
1295
|
end
|
|
912
1296
|
|
|
@@ -925,8 +1309,8 @@ describe 'Generated TraceHelper' do
|
|
|
925
1309
|
.to eq([['navigate', 'https://example.test/login'], ['navigate', 'https://example.test/login']])
|
|
926
1310
|
end
|
|
927
1311
|
|
|
928
|
-
context 'with VIDEO=off and
|
|
929
|
-
let(:env) { { 'VIDEO' => 'off', 'TRACE' => 'off' } }
|
|
1312
|
+
context 'with VIDEO=off, TRACE=off and BIDI=off' do
|
|
1313
|
+
let(:env) { { 'VIDEO' => 'off', 'TRACE' => 'off', 'BIDI' => 'off' } }
|
|
930
1314
|
|
|
931
1315
|
it 'passes no listener at all' do
|
|
932
1316
|
expect(created_arguments.keys).to eq([:options])
|
|
@@ -937,9 +1321,17 @@ describe 'Generated TraceHelper' do
|
|
|
937
1321
|
end
|
|
938
1322
|
end
|
|
939
1323
|
|
|
940
|
-
it 'sets web_socket_url only for chrome, edge and firefox
|
|
1324
|
+
it 'sets web_socket_url only for chrome, edge and firefox' do
|
|
941
1325
|
expect(%w[chrome edge firefox safari].map { |browser| options_for(browser).web_socket_url }).to eq([true, true, true, nil])
|
|
942
1326
|
end
|
|
1327
|
+
|
|
1328
|
+
context 'with TRACE=off' do
|
|
1329
|
+
let(:env) { { 'TRACE' => 'off' } }
|
|
1330
|
+
|
|
1331
|
+
it 'still sets web_socket_url for the network mocks' do
|
|
1332
|
+
expect(options_for('chrome').web_socket_url).to be(true)
|
|
1333
|
+
end
|
|
1334
|
+
end
|
|
943
1335
|
end
|
|
944
1336
|
end
|
|
945
1337
|
# rubocop:enable RSpec/MultipleMemoizedHelpers, RSpec/VerifiedDoubles, RSpec/ExpectOutput, RSpec/MultipleExpectations, RSpec/ExampleLength
|