ruby_raider 3.12.1 → 3.13.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/README.md +2 -0
- data/lib/commands/utility_commands.rb +16 -0
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +65 -1
- data/lib/generators/generator.rb +10 -1
- data/lib/generators/helper_generator.rb +7 -0
- data/lib/generators/templates/common/gemfile.tt +1 -1
- data/lib/generators/templates/common/partials/web_config.tt +7 -0
- data/lib/generators/templates/common/read_me.tt +45 -0
- data/lib/generators/templates/helpers/driver_helper.tt +22 -0
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +11 -1
- data/lib/generators/templates/helpers/raider_element.tt +4 -4
- data/lib/generators/templates/helpers/spec_helper.tt +19 -0
- data/lib/generators/templates/helpers/trace_helper.tt +1097 -0
- data/lib/generators/templates/helpers/wait_helper.tt +145 -4
- data/lib/utilities/utilities.rb +7 -0
- data/lib/version +1 -1
- data/ruby_raider.gemspec +1 -0
- data/sig/commands/utility_commands.rbs +2 -0
- data/sig/generators/generator.rbs +4 -0
- data/sig/generators/helper_generator.rbs +1 -0
- data/sig/utilities/utilities.rbs +1 -0
- data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +11 -2
- data/spec/generators/trace_helper_spec.rb +945 -0
- data/spec/integration/content/config_content_spec.rb +20 -0
- data/spec/integration/content/cucumber_reporter_content_spec.rb +51 -0
- data/spec/integration/content/gemfile_content_spec.rb +2 -2
- data/spec/integration/content/helper_content_spec.rb +60 -1
- data/spec/integration/content/readme_content_spec.rb +46 -0
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/end_to_end_spec.rb +392 -29
- data/spec/integration/generators/helpers_generator_spec.rb +26 -0
- data/spec/integration/helpers/wait_helper_spec.rb +183 -0
- data/spec/integration/trace_format_checker.rb +141 -0
- data/spec/integration/trace_page_server.rb +80 -0
- data/spec/utilities/trace_command_spec.rb +66 -0
- metadata +27 -2
|
@@ -6,6 +6,8 @@ require 'open3'
|
|
|
6
6
|
require 'json'
|
|
7
7
|
require 'erb'
|
|
8
8
|
require 'tmpdir'
|
|
9
|
+
require_relative 'trace_format_checker'
|
|
10
|
+
require_relative 'trace_page_server'
|
|
9
11
|
|
|
10
12
|
# End-to-end integration tests that verify generated projects actually work
|
|
11
13
|
# These tests:
|
|
@@ -455,6 +457,36 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
455
457
|
end
|
|
456
458
|
end
|
|
457
459
|
|
|
460
|
+
# Browser e2e helpers shared by the failure video and the trace replay contexts
|
|
461
|
+
def browser_installed?(browser)
|
|
462
|
+
if browser == 'firefox'
|
|
463
|
+
ENV['FIREFOX_BINARY'] ? File.executable?(ENV['FIREFOX_BINARY']) : app_or_command?('Firefox', 'firefox')
|
|
464
|
+
else
|
|
465
|
+
app_or_command?('Google Chrome', 'google-chrome') || app_or_command?('Chromium', 'chromium')
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def app_or_command?(app, command)
|
|
470
|
+
File.directory?("/Applications/#{app}.app") || system("command -v #{command} > /dev/null 2>&1")
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# A separate project, so the default projects keep resolving raider_reporter 0.10+ from RubyGems
|
|
474
|
+
def generate_video_project(framework, automation)
|
|
475
|
+
name = "video_#{framework}_#{automation}"
|
|
476
|
+
unless LazyProjectGenerator::GENERATED.include?(name)
|
|
477
|
+
generate_framework(framework:, automation:, name:)
|
|
478
|
+
LazyProjectGenerator::GENERATED.add(name)
|
|
479
|
+
use_local_ruby_raider(name)
|
|
480
|
+
expect(run_in_project(name, 'bundle install --quiet', timeout: 300)[:success]).to be(true)
|
|
481
|
+
end
|
|
482
|
+
FileUtils.rm_rf(File.join(name, 'raider-results'))
|
|
483
|
+
name
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def results(project)
|
|
487
|
+
JSON.parse(File.read(File.join(project, 'raider-results', 'run.json')))['results']
|
|
488
|
+
end
|
|
489
|
+
|
|
458
490
|
# Failure video replay against real browsers. Needs an ffmpeg with libvpx (RAIDER_FFMPEG_PATH), because the
|
|
459
491
|
# RaiderHQ ffmpeg release has no checksums yet. Skipped when it, or the browser, is missing. Firefox can be
|
|
460
492
|
# pointed at a specific binary with FIREFOX_BINARY.
|
|
@@ -470,31 +502,6 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
470
502
|
skip "#{browser} is not installed" unless browser_installed?(browser)
|
|
471
503
|
end
|
|
472
504
|
|
|
473
|
-
def browser_installed?(browser)
|
|
474
|
-
if browser == 'firefox'
|
|
475
|
-
ENV['FIREFOX_BINARY'] ? File.executable?(ENV['FIREFOX_BINARY']) : app_or_command?('Firefox', 'firefox')
|
|
476
|
-
else
|
|
477
|
-
app_or_command?('Google Chrome', 'google-chrome') || app_or_command?('Chromium', 'chromium')
|
|
478
|
-
end
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
def app_or_command?(app, command)
|
|
482
|
-
File.directory?("/Applications/#{app}.app") || system("command -v #{command} > /dev/null 2>&1")
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
# A separate project, so the default projects keep resolving raider_reporter 0.10+ from RubyGems
|
|
486
|
-
def generate_video_project(framework, automation)
|
|
487
|
-
name = "video_#{framework}_#{automation}"
|
|
488
|
-
unless LazyProjectGenerator::GENERATED.include?(name)
|
|
489
|
-
generate_framework(framework:, automation:, name:)
|
|
490
|
-
LazyProjectGenerator::GENERATED.add(name)
|
|
491
|
-
use_local_ruby_raider(name)
|
|
492
|
-
expect(run_in_project(name, 'bundle install --quiet', timeout: 300)[:success]).to be(true)
|
|
493
|
-
end
|
|
494
|
-
FileUtils.rm_rf(File.join(name, 'raider-results'))
|
|
495
|
-
name
|
|
496
|
-
end
|
|
497
|
-
|
|
498
505
|
def video_env(overrides = {})
|
|
499
506
|
{ 'HEADLESS' => 'true', 'RAIDER_FFMPEG_PATH' => ffmpeg_path, 'RAIDER_CACHE_DIR' => cache_dir,
|
|
500
507
|
'RAIDER_TOKEN' => nil, 'RAIDER_ENDPOINT' => nil, 'VIDEO' => nil }.merge(overrides)
|
|
@@ -562,10 +569,6 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
562
569
|
RUBY
|
|
563
570
|
end
|
|
564
571
|
|
|
565
|
-
def results(project)
|
|
566
|
-
JSON.parse(File.read(File.join(project, 'raider-results', 'run.json')))['results']
|
|
567
|
-
end
|
|
568
|
-
|
|
569
572
|
def video_files(project)
|
|
570
573
|
Dir.glob(File.join(project, 'raider-results', '**', '*.{webm,mp4}'))
|
|
571
574
|
end
|
|
@@ -657,6 +660,366 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
657
660
|
end
|
|
658
661
|
end
|
|
659
662
|
|
|
663
|
+
# Trace replay against headless Chrome, and one example on headless Firefox. The page is served over HTTP from this process (never file://), so
|
|
664
|
+
# WebDriver BiDi reports real network and console entries. raider_reporter ~> 0.12 resolves from RubyGems, or
|
|
665
|
+
# from a checkout at RAIDER_REPORTER_PATH. The example with video on also needs RAIDER_FFMPEG_PATH.
|
|
666
|
+
# Set TRACE_FIXTURE_DIR to keep the failing RSpec example's trace.zip (and an unzipped copy) there.
|
|
667
|
+
context 'Trace replay', :slow do
|
|
668
|
+
let!(:server) { TracePageServer.new }
|
|
669
|
+
let(:cache_dir) { Dir.mktmpdir('raider-cache') }
|
|
670
|
+
|
|
671
|
+
after do
|
|
672
|
+
server.stop
|
|
673
|
+
FileUtils.rm_rf(cache_dir)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Typed into the secret fields; neither may reach the zip.
|
|
677
|
+
def secrets
|
|
678
|
+
%w[hunter2 s3cr3t]
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Typed into a plain field; it is kept as the step's value.
|
|
682
|
+
def plain_value
|
|
683
|
+
'plain-trace-name'
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def skip_unless_trace_prerequisites(project, browser = 'chrome')
|
|
687
|
+
skip "#{browser == 'chrome' ? 'Google Chrome' : 'Firefox'} is not installed" unless browser_installed?(browser)
|
|
688
|
+
version = run_in_project(project, %(bundle exec ruby -e "require 'raider_reporter/version'; print RaiderReporter::VERSION"))
|
|
689
|
+
skip "the bundle does not resolve raider_reporter ~> 0.12 (set RAIDER_REPORTER_PATH): #{version[:stderr].lines.last}" unless
|
|
690
|
+
version[:success] && Gem::Version.new(version[:stdout]) >= Gem::Version.new('0.12')
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def trace_env(overrides = {})
|
|
694
|
+
{ 'HEADLESS' => 'true', 'VIDEO' => 'off', 'TRACE' => nil, 'RAIDER_CACHE_DIR' => cache_dir,
|
|
695
|
+
'RAIDER_TOKEN' => nil, 'RAIDER_ENDPOINT' => nil }.merge(overrides)
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def trace_attachments(result)
|
|
699
|
+
result['attachments'].select { |attachment| attachment['name'] == 'trace' }
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def trace_files(project)
|
|
703
|
+
Dir.glob(File.join(project, 'raider-results', '**', '*trace.zip'))
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def trace_zip(project, attachment)
|
|
707
|
+
File.join(project, 'raider-results', attachment['path'])
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# { entry name => bytes } for every entry of the zip.
|
|
711
|
+
def zip_entries(path)
|
|
712
|
+
Zip::File.open(path) { |zip| zip.entries.to_h { |entry| [entry.name, entry.get_input_stream.read] } }
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def result_named(project, name)
|
|
716
|
+
results(project).find { |result| result['full_name'].include?(name) }
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def expect_run(result, exit_code: 1)
|
|
720
|
+
expect(result[:exit_code]).to eq(exit_code), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
|
|
721
|
+
expect(result[:stderr]).not_to include('[trace]')
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def expect_valid_trace(path)
|
|
725
|
+
expect(TraceFormatChecker.problems(path)).to be_empty
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def keep_fixture(path)
|
|
729
|
+
dir = ENV.fetch('TRACE_FIXTURE_DIR', nil)
|
|
730
|
+
return unless dir
|
|
731
|
+
|
|
732
|
+
FileUtils.rm_rf(File.join(dir, 'unzipped'))
|
|
733
|
+
FileUtils.mkdir_p(File.join(dir, 'unzipped'))
|
|
734
|
+
FileUtils.cp(path, File.join(dir, 'trace.zip'))
|
|
735
|
+
zip_entries(path).each do |name, bytes|
|
|
736
|
+
FileUtils.mkdir_p(File.dirname(File.join(dir, 'unzipped', name)))
|
|
737
|
+
File.binwrite(File.join(dir, 'unzipped', name), bytes)
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# The RSpec examples' names, spec file and readers, shared by the Chrome and Firefox examples.
|
|
742
|
+
def failing
|
|
743
|
+
'fails after driving the page'
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def passing
|
|
747
|
+
'passes after driving the page'
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Drives the page through the generated WaitHelper, so the trace has action steps.
|
|
751
|
+
# Each example prints the driver's webSocketUrl, which is set only while tracing is on.
|
|
752
|
+
def add_rspec_trace_spec(project)
|
|
753
|
+
File.write(File.join(project, 'spec/trace_replay_spec.rb'), <<~SPEC)
|
|
754
|
+
# frozen_string_literal: true
|
|
755
|
+
|
|
756
|
+
require_relative '../helpers/spec_helper'
|
|
757
|
+
|
|
758
|
+
describe 'Trace replay' do
|
|
759
|
+
it '#{failing}' do
|
|
760
|
+
driver.navigate.to('#{server.url}')
|
|
761
|
+
puts "TRACE_E2E_WEBSOCKET=\#{driver.capabilities[:web_socket_url].inspect}"
|
|
762
|
+
send_keys({ id: 'pw' }, '#{secrets[0]}')
|
|
763
|
+
send_keys({ id: 'pw2' }, '#{secrets[1]}')
|
|
764
|
+
find_element(id: 'name').send_keys('#{plain_value}')
|
|
765
|
+
click({ id: 'go' })
|
|
766
|
+
eventually(timeout: 5) { expect(text(id: 'status')).to eq('answered by /api') }
|
|
767
|
+
eventually(timeout: 1) { expect(text(id: 'title')).to eq('deliberate trace failure') }
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
it '#{passing}' do
|
|
771
|
+
driver.navigate.to('#{server.url}')
|
|
772
|
+
puts "TRACE_E2E_WEBSOCKET=\#{driver.capabilities[:web_socket_url].inspect}"
|
|
773
|
+
send_keys({ id: 'name' }, 'ok')
|
|
774
|
+
eventually { expect(text(id: 'title')).to eq('Trace') }
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
SPEC
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def failing_trace(result)
|
|
781
|
+
expect(trace_attachments(result).size).to eq(1)
|
|
782
|
+
trace_zip(project, trace_attachments(result).first)
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def steps_of(path)
|
|
786
|
+
TraceFormatChecker.read(path).first['steps']
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# BiDi screenshots, so window_handle worked as the browsing context id: .jpg entries that are JPEGs.
|
|
790
|
+
def expect_jpeg_screenshots(path, names)
|
|
791
|
+
screenshots = names.grep(%r{\Asteps/\d{4}\.(jpg|png)\z})
|
|
792
|
+
expect(screenshots).not_to be_empty
|
|
793
|
+
expect(screenshots).to all(end_with('.jpg'))
|
|
794
|
+
entries = zip_entries(path)
|
|
795
|
+
screenshots.each { |name| expect(entries[name].byteslice(0, 2).bytes).to eq([0xFF, 0xD8]), name }
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
describe 'Selenium + RSpec on headless Chrome' do
|
|
799
|
+
let(:project) { 'video_rspec_selenium' }
|
|
800
|
+
let(:command) { 'bundle exec rspec spec/trace_replay_spec.rb' }
|
|
801
|
+
|
|
802
|
+
before do
|
|
803
|
+
generate_video_project('rspec', 'selenium')
|
|
804
|
+
skip_unless_trace_prerequisites(project)
|
|
805
|
+
add_rspec_trace_spec(project)
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
after { FileUtils.rm_f(File.join(project, 'spec/trace_replay_spec.rb')) }
|
|
809
|
+
|
|
810
|
+
# A navigate step (Selenium sends get through BiDi with web_socket_url set), and action steps whose
|
|
811
|
+
# elements have a label and a rect.
|
|
812
|
+
def expect_steps(steps)
|
|
813
|
+
expect(steps.map { |step| step['kind'] }).to include('navigate')
|
|
814
|
+
actions = steps.select { |step| %w[click send_keys].include?(step['action']) }
|
|
815
|
+
expect(actions.size).to eq(4)
|
|
816
|
+
expect(actions).to all(include('element' => include('label' => a_kind_of(String)), 'rect' => a_kind_of(Hash)))
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def expect_network_and_console(trace)
|
|
820
|
+
api = trace['network'].find { |entry| entry['url'].to_s.end_with?('/api') }
|
|
821
|
+
expect(api).to include('method' => 'GET', 'status' => 200, 'mime_type' => 'application/json')
|
|
822
|
+
expect(api['bytes']).to be_positive
|
|
823
|
+
expect(api['duration_ms']).to be >= (TracePageServer::API_DELAY * 1000) - 50
|
|
824
|
+
expect(trace['console']).to include(include('level' => 'error', 'text' => a_string_including('trace e2e error')))
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# The same-origin stylesheet inlined as <style>, and the page's <script> gone.
|
|
828
|
+
def expect_sanitised_snapshots(path, names)
|
|
829
|
+
entries = zip_entries(path)
|
|
830
|
+
snapshots = names.grep(%r{\Asteps/\d{4}\.html\z}).map { |name| entries[name] }
|
|
831
|
+
expect(snapshots).not_to be_empty
|
|
832
|
+
expect(snapshots).to all(match(/<style[^>]*>[^<]*#{Regexp.escape(TracePageServer::CSS_RULE)}/))
|
|
833
|
+
expect(snapshots).to all(satisfy { |html| !html.match?(/<script/i) })
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
it 'attaches one trace.zip to the failing example only, valid against raider-trace v1, with network and console',
|
|
837
|
+
:aggregate_failures do
|
|
838
|
+
run = run_in_project(project, command, env: trace_env)
|
|
839
|
+
expect_run(run)
|
|
840
|
+
failed = result_named(project, failing)
|
|
841
|
+
attachment = trace_attachments(failed).first
|
|
842
|
+
path = failing_trace(failed)
|
|
843
|
+
keep_fixture(path)
|
|
844
|
+
|
|
845
|
+
expect(run[:stdout]).to include('TRACE_E2E_WEBSOCKET="ws://')
|
|
846
|
+
expect(attachment).to include('content_type' => 'application/vnd.raider.trace+zip', 'attempt' => 1)
|
|
847
|
+
expect(trace_attachments(result_named(project, passing))).to be_empty
|
|
848
|
+
expect(trace_files(project).size).to eq(1)
|
|
849
|
+
expect_valid_trace(path)
|
|
850
|
+
|
|
851
|
+
trace, names = TraceFormatChecker.read(path)
|
|
852
|
+
expect(trace['capabilities']).to include('network' => true, 'console' => true)
|
|
853
|
+
expect_steps(trace['steps'])
|
|
854
|
+
expect_jpeg_screenshots(path, names)
|
|
855
|
+
expect_network_and_console(trace)
|
|
856
|
+
expect_sanitised_snapshots(path, names)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
it 'masks the typed password', :aggregate_failures do
|
|
860
|
+
run = run_in_project(project, command, env: trace_env)
|
|
861
|
+
expect_run(run)
|
|
862
|
+
path = failing_trace(result_named(project, failing))
|
|
863
|
+
|
|
864
|
+
zip_entries(path).each do |name, bytes|
|
|
865
|
+
secrets.each { |secret| expect(bytes.b).not_to include(secret.b), "#{secret} is in #{name}" }
|
|
866
|
+
end
|
|
867
|
+
typed = steps_of(path).select { |step| step['action'] == 'send_keys' }
|
|
868
|
+
expect(typed.size).to eq(3)
|
|
869
|
+
expect(typed.first(2)).to all(include('value' => nil, 'secret' => true))
|
|
870
|
+
expect(typed.last).to include('value' => plain_value, 'secret' => false)
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
it 'writes no trace and no web_socket_url with TRACE=off', :aggregate_failures do
|
|
874
|
+
run = run_in_project(project, command, env: trace_env('TRACE' => 'off'))
|
|
875
|
+
expect_run(run)
|
|
876
|
+
expect(run[:stdout].scan(/^TRACE_E2E_WEBSOCKET=(.*)$/).flatten).to eq(%w[nil nil])
|
|
877
|
+
expect(results(project).flat_map { |result| trace_attachments(result) }).to be_empty
|
|
878
|
+
expect(trace_files(project)).to be_empty
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
it 'keeps traces for passing examples too with TRACE=on', :aggregate_failures do
|
|
882
|
+
run = run_in_project(project, command, env: trace_env('TRACE' => 'on'))
|
|
883
|
+
expect_run(run)
|
|
884
|
+
[failing, passing].each do |name|
|
|
885
|
+
attachments = trace_attachments(result_named(project, name))
|
|
886
|
+
expect(attachments.size).to eq(1), "#{name} has #{attachments.size} traces"
|
|
887
|
+
expect_valid_trace(trace_zip(project, attachments.first)) if attachments.one?
|
|
888
|
+
end
|
|
889
|
+
expect(trace_files(project).size).to eq(2)
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
# Spec §2 Cost: tracing on failing tests (the default) costs under 15% of the wall-clock time.
|
|
893
|
+
# One warm-up run, then the modes interleaved, three runs each, compared by their medians.
|
|
894
|
+
# Opt-in: CI runs the integration specs on every PR, and runner jitter is the size of the margin.
|
|
895
|
+
it 'keeps the suite within 15% of its TRACE=off wall-clock time' do
|
|
896
|
+
skip 'set TRACE_OVERHEAD=1 to measure trace overhead' unless ENV['TRACE_OVERHEAD']
|
|
897
|
+
|
|
898
|
+
run_in_project(project, command, env: trace_env)
|
|
899
|
+
times = Hash.new { |hash, key| hash[key] = [] }
|
|
900
|
+
3.times do
|
|
901
|
+
{ 'off' => 'off', 'default' => nil }.each do |mode, value|
|
|
902
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
903
|
+
expect_run(run_in_project(project, command, env: trace_env('TRACE' => value)))
|
|
904
|
+
times[mode] << (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
off, traced = times.values_at('off', 'default').map { |runs| runs.sort[1] }
|
|
908
|
+
overhead = (traced - off) / off
|
|
909
|
+
puts format("\n[trace overhead] TRACE=off %<off>s, default %<default>s, medians %<o>.2fs vs %<t>.2fs: %<p>+.1f%%",
|
|
910
|
+
off: times['off'].map { |time| time.round(2) }, default: times['default'].map { |time| time.round(2) },
|
|
911
|
+
o: off, t: traced, p: overhead * 100)
|
|
912
|
+
expect(overhead).to be < 0.15
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
it 'attaches video.webm and trace.zip to the failing example with video on', :aggregate_failures do
|
|
916
|
+
ffmpeg = ENV.fetch('RAIDER_FFMPEG_PATH', '')
|
|
917
|
+
skip 'RAIDER_FFMPEG_PATH is not set to an executable ffmpeg with libvpx' unless
|
|
918
|
+
File.file?(ffmpeg) && File.executable?(ffmpeg)
|
|
919
|
+
|
|
920
|
+
run = run_in_project(project, command, env: trace_env('VIDEO' => nil, 'RAIDER_FFMPEG_PATH' => ffmpeg))
|
|
921
|
+
expect_run(run)
|
|
922
|
+
expect(run[:stderr]).not_to include('[video]')
|
|
923
|
+
attachments = result_named(project, failing)['attachments']
|
|
924
|
+
expect(attachments.map { |attachment| attachment['content_type'] })
|
|
925
|
+
.to include('video/webm', 'application/vnd.raider.trace+zip')
|
|
926
|
+
expect(attachments.map { |attachment| File.basename(attachment['path']) })
|
|
927
|
+
.to include(a_string_ending_with('video.webm'), a_string_ending_with('trace.zip'))
|
|
928
|
+
expect_valid_trace(failing_trace(result_named(project, failing)))
|
|
929
|
+
end
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
# The same RSpec project with config.yml set to firefox; Selenium Manager fetches geckodriver.
|
|
933
|
+
describe 'Selenium + RSpec on headless Firefox' do
|
|
934
|
+
let(:project) { 'video_rspec_selenium' }
|
|
935
|
+
let(:config) { File.join(project, 'config/config.yml') }
|
|
936
|
+
# Read before the switch to firefox and written back after, so the Chrome examples keep chrome.
|
|
937
|
+
let(:original_config) { File.read(config) }
|
|
938
|
+
|
|
939
|
+
before do
|
|
940
|
+
generate_video_project('rspec', 'selenium')
|
|
941
|
+
skip_unless_trace_prerequisites(project, 'firefox')
|
|
942
|
+
File.write(config, original_config.sub(/^browser: .*$/, 'browser: firefox'))
|
|
943
|
+
add_rspec_trace_spec(project)
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
after do
|
|
947
|
+
File.write(config, original_config) if File.exist?(config)
|
|
948
|
+
FileUtils.rm_f(File.join(project, 'spec/trace_replay_spec.rb'))
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
it 'attaches a trace.zip valid against raider-trace v1, with network, console and BiDi JPEG screenshots',
|
|
952
|
+
:aggregate_failures do
|
|
953
|
+
run = run_in_project(project, 'bundle exec rspec spec/trace_replay_spec.rb', env: trace_env, timeout: 240)
|
|
954
|
+
expect_run(run)
|
|
955
|
+
path = failing_trace(result_named(project, failing))
|
|
956
|
+
expect_valid_trace(path)
|
|
957
|
+
|
|
958
|
+
trace, names = TraceFormatChecker.read(path)
|
|
959
|
+
expect(trace['browser']).to include('name' => 'firefox')
|
|
960
|
+
expect(trace['capabilities']).to include('network' => true, 'console' => true)
|
|
961
|
+
expect_jpeg_screenshots(path, names)
|
|
962
|
+
api = trace['network'].find { |entry| entry['url'].to_s.end_with?('/api') }
|
|
963
|
+
expect(api).to include('status' => 200)
|
|
964
|
+
expect(api && api['duration_ms']).to be_a(Integer).and be_positive
|
|
965
|
+
end
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
describe 'Selenium + Cucumber on headless Chrome' do
|
|
969
|
+
let(:project) { 'video_cucumber_selenium' }
|
|
970
|
+
|
|
971
|
+
before do
|
|
972
|
+
generate_video_project('cucumber', 'selenium')
|
|
973
|
+
skip_unless_trace_prerequisites(project)
|
|
974
|
+
add_cucumber_trace_feature(project)
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
after do
|
|
978
|
+
FileUtils.rm_f(File.join(project, 'features/trace_replay.feature'))
|
|
979
|
+
FileUtils.rm_f(File.join(project, 'features/step_definitions/trace_replay_steps.rb'))
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
def add_cucumber_trace_feature(project)
|
|
983
|
+
File.write(File.join(project, 'features/trace_replay.feature'), <<~FEATURE)
|
|
984
|
+
Feature: Trace replay
|
|
985
|
+
|
|
986
|
+
Scenario: A failing traced scenario
|
|
987
|
+
Given the trace page is open
|
|
988
|
+
When I type a name on the trace page
|
|
989
|
+
Then the trace page title is "deliberate trace failure"
|
|
990
|
+
FEATURE
|
|
991
|
+
File.write(File.join(project, 'features/step_definitions/trace_replay_steps.rb'), <<~STEPS)
|
|
992
|
+
# frozen_string_literal: true
|
|
993
|
+
|
|
994
|
+
Given('the trace page is open') do
|
|
995
|
+
driver.navigate.to('#{server.url}')
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
When('I type a name on the trace page') do
|
|
999
|
+
send_keys({ id: 'name' }, '#{plain_value}')
|
|
1000
|
+
click({ id: 'go' })
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
Then('the trace page title is {string}') do |title|
|
|
1004
|
+
eventually(timeout: 1) { expect(text(id: 'title')).to eq(title) }
|
|
1005
|
+
end
|
|
1006
|
+
STEPS
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
it 'attaches a trace.zip with Gherkin step groups to the failing scenario', :aggregate_failures do
|
|
1010
|
+
run = run_in_project(project, 'bundle exec cucumber features/trace_replay.feature', env: trace_env, timeout: 240)
|
|
1011
|
+
expect_run(run)
|
|
1012
|
+
attachments = trace_attachments(result_named(project, 'A failing traced scenario'))
|
|
1013
|
+
expect(attachments.size).to eq(1)
|
|
1014
|
+
expect(attachments.first).to include('content_type' => 'application/vnd.raider.trace+zip', 'attempt' => 1)
|
|
1015
|
+
path = trace_zip(project, attachments.first)
|
|
1016
|
+
expect_valid_trace(path)
|
|
1017
|
+
groups = TraceFormatChecker.read(path).first['steps'].select { |step| step['action'] == 'cucumber_step' }
|
|
1018
|
+
expect(groups).to include(include('kind' => 'group', 'name' => a_string_including('the trace page is open')))
|
|
1019
|
+
end
|
|
1020
|
+
end
|
|
1021
|
+
end
|
|
1022
|
+
|
|
660
1023
|
# Mobile and Visual frameworks require external services, so we only verify structure
|
|
661
1024
|
context 'Mobile Automation Frameworks (structure validation only)' do
|
|
662
1025
|
describe 'iOS + RSpec' do
|
|
@@ -46,6 +46,18 @@ describe HelpersGenerator do
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
shared_examples 'creates the trace helper' do |name|
|
|
50
|
+
it 'creates the trace helper' do
|
|
51
|
+
expect(File).to exist("#{name}/helpers/trace_helper.rb")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
shared_examples 'skips the trace helper' do |name|
|
|
56
|
+
it 'does not create the trace helper' do
|
|
57
|
+
expect(File).not_to exist("#{name}/helpers/trace_helper.rb")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
49
61
|
shared_examples 'creates rspec helpers' do |name|
|
|
50
62
|
it 'creates a spec helper file' do
|
|
51
63
|
expect(File).to exist("#{name}/helpers/spec_helper.rb")
|
|
@@ -66,6 +78,7 @@ describe HelpersGenerator do
|
|
|
66
78
|
|
|
67
79
|
context 'with rspec and selenium' do
|
|
68
80
|
include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
81
|
+
include_examples 'creates the trace helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
69
82
|
include_examples 'creates a session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
70
83
|
include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
71
84
|
include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
@@ -74,6 +87,7 @@ describe HelpersGenerator do
|
|
|
74
87
|
|
|
75
88
|
context 'with rspec and watir' do
|
|
76
89
|
include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
90
|
+
include_examples 'skips the trace helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
77
91
|
include_examples 'creates a session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
78
92
|
include_examples 'creates watir helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
79
93
|
include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
@@ -81,6 +95,7 @@ describe HelpersGenerator do
|
|
|
81
95
|
|
|
82
96
|
context 'with cucumber and selenium' do
|
|
83
97
|
include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
|
|
98
|
+
include_examples 'creates the trace helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
|
|
84
99
|
include_examples 'creates a session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
|
|
85
100
|
include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
|
|
86
101
|
include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
|
|
@@ -89,6 +104,7 @@ describe HelpersGenerator do
|
|
|
89
104
|
|
|
90
105
|
context 'with cucumber and watir' do
|
|
91
106
|
include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}"
|
|
107
|
+
include_examples 'skips the trace helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}"
|
|
92
108
|
include_examples 'creates a session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}"
|
|
93
109
|
include_examples 'creates watir helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}"
|
|
94
110
|
include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}"
|
|
@@ -96,6 +112,7 @@ describe HelpersGenerator do
|
|
|
96
112
|
|
|
97
113
|
context 'with rspec and appium android' do
|
|
98
114
|
include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
|
|
115
|
+
include_examples 'skips the trace helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
|
|
99
116
|
include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
|
|
100
117
|
include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
|
|
101
118
|
include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
|
|
@@ -104,6 +121,7 @@ describe HelpersGenerator do
|
|
|
104
121
|
|
|
105
122
|
context 'with rspec and appium ios' do
|
|
106
123
|
include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
|
|
124
|
+
include_examples 'skips the trace helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
|
|
107
125
|
include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
|
|
108
126
|
include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
|
|
109
127
|
include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
|
|
@@ -112,6 +130,7 @@ describe HelpersGenerator do
|
|
|
112
130
|
|
|
113
131
|
context 'with cucumber and appium android' do
|
|
114
132
|
include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
|
|
133
|
+
include_examples 'skips the trace helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
|
|
115
134
|
include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
|
|
116
135
|
include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
|
|
117
136
|
include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
|
|
@@ -120,6 +139,7 @@ describe HelpersGenerator do
|
|
|
120
139
|
|
|
121
140
|
context 'with cucumber and appium ios' do
|
|
122
141
|
include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
|
|
142
|
+
include_examples 'skips the trace helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
|
|
123
143
|
include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
|
|
124
144
|
include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
|
|
125
145
|
include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
|
|
@@ -128,6 +148,7 @@ describe HelpersGenerator do
|
|
|
128
148
|
|
|
129
149
|
context 'with rspec and appium cross platform' do
|
|
130
150
|
include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
151
|
+
include_examples 'skips the trace helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
131
152
|
include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
132
153
|
include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
133
154
|
include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
@@ -137,6 +158,7 @@ describe HelpersGenerator do
|
|
|
137
158
|
|
|
138
159
|
context 'with cucumber and appium cross platform' do
|
|
139
160
|
include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
161
|
+
include_examples 'skips the trace helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
140
162
|
include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
141
163
|
include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
142
164
|
include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
@@ -153,6 +175,10 @@ describe HelpersGenerator do
|
|
|
153
175
|
expect(File).to exist("#{name}/helpers/wait_helper.rb")
|
|
154
176
|
expect(File).to exist("#{name}/helpers/raider_element.rb")
|
|
155
177
|
end
|
|
178
|
+
|
|
179
|
+
it "#{automation == 'selenium' ? 'creates' : 'skips'} the trace helper" do
|
|
180
|
+
expect(File.exist?("#{name}/helpers/trace_helper.rb")).to eq(automation == 'selenium')
|
|
181
|
+
end
|
|
156
182
|
end
|
|
157
183
|
end
|
|
158
184
|
|