ruby_raider 3.12.0 → 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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/README.md +2 -0
  4. data/lib/commands/utility_commands.rb +16 -0
  5. data/lib/generators/automation/templates/page.tt +8 -3
  6. data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -3
  7. data/lib/generators/automation/templates/partials/url_methods.tt +3 -2
  8. data/lib/generators/automation/templates/partials/visit_method.tt +5 -5
  9. data/lib/generators/cucumber/cucumber_generator.rb +1 -1
  10. data/lib/generators/cucumber/templates/env.tt +3 -3
  11. data/lib/generators/cucumber/templates/partials/selenium_env.tt +65 -1
  12. data/lib/generators/cucumber/templates/steps.tt +2 -2
  13. data/lib/generators/cucumber/templates/user_factory.tt +2 -0
  14. data/lib/generators/cucumber/templates/world.tt +5 -3
  15. data/lib/generators/generator.rb +25 -12
  16. data/lib/generators/helper_generator.rb +7 -0
  17. data/lib/generators/rspec/templates/accessibility_spec.tt +10 -10
  18. data/lib/generators/rspec/templates/spec.tt +2 -1
  19. data/lib/generators/templates/common/gemfile.tt +9 -9
  20. data/lib/generators/templates/common/partials/web_config.tt +7 -0
  21. data/lib/generators/templates/common/read_me.tt +45 -0
  22. data/lib/generators/templates/helpers/appium_helper.tt +17 -15
  23. data/lib/generators/templates/helpers/browser_helper.tt +1 -2
  24. data/lib/generators/templates/helpers/driver_helper.tt +22 -0
  25. data/lib/generators/templates/helpers/partials/appium_driver.tt +1 -1
  26. data/lib/generators/templates/helpers/partials/selenium_driver.tt +11 -1
  27. data/lib/generators/templates/helpers/raider_element.tt +4 -4
  28. data/lib/generators/templates/helpers/session_state.tt +1 -1
  29. data/lib/generators/templates/helpers/spec_helper.tt +20 -1
  30. data/lib/generators/templates/helpers/trace_helper.tt +1097 -0
  31. data/lib/generators/templates/helpers/wait_helper.tt +145 -4
  32. data/lib/ruby_raider.rb +9 -2
  33. data/lib/utilities/utilities.rb +7 -0
  34. data/lib/version +1 -1
  35. data/ruby_raider.gemspec +1 -0
  36. data/sig/commands/utility_commands.rbs +2 -0
  37. data/sig/generators/generator.rbs +6 -0
  38. data/sig/generators/helper_generator.rbs +1 -0
  39. data/sig/ruby_raider.rbs +1 -0
  40. data/sig/utilities/utilities.rbs +1 -0
  41. data/spec/commands/raider_commands_spec.rb +25 -1
  42. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +1 -1
  43. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +2 -4
  44. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +2 -4
  45. data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +11 -2
  46. data/spec/generators/trace_helper_spec.rb +945 -0
  47. data/spec/integration/content/config_content_spec.rb +20 -0
  48. data/spec/integration/content/content_helper.rb +1 -5
  49. data/spec/integration/content/cucumber_reporter_content_spec.rb +51 -0
  50. data/spec/integration/content/gemfile_content_spec.rb +2 -2
  51. data/spec/integration/content/helper_content_spec.rb +60 -1
  52. data/spec/integration/content/readme_content_spec.rb +46 -0
  53. data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
  54. data/spec/integration/content/rubocop_content_spec.rb +87 -0
  55. data/spec/integration/content/syntax_validation_spec.rb +31 -51
  56. data/spec/integration/content/test_content_spec.rb +5 -2
  57. data/spec/integration/end_to_end_spec.rb +415 -29
  58. data/spec/integration/frozen_string_literal_matcher.rb +13 -0
  59. data/spec/integration/generators/helpers_generator_spec.rb +26 -0
  60. data/spec/integration/generators/invoke_generators_spec.rb +60 -0
  61. data/spec/integration/helpers/wait_helper_spec.rb +183 -0
  62. data/spec/integration/scaffolding_e2e_spec.rb +1 -5
  63. data/spec/integration/trace_format_checker.rb +141 -0
  64. data/spec/integration/trace_page_server.rb +80 -0
  65. data/spec/utilities/trace_command_spec.rb +66 -0
  66. metadata +29 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3dd26171963a22a6a95ac06de57c7baf3e462a8d0d71049b88b99d40552c94c0
4
- data.tar.gz: 9826176205e621abddb2ddd583aa3bdee28eacd3c5e7d00a606bda9b2ba443e1
3
+ metadata.gz: 6e4d2e6fae8f8504215b558818a81660fca9652f52886c6b72f3e1d6850d01bd
4
+ data.tar.gz: 3a8e5a4290303aab776bce5e6e9e8c86023e88a036b935de18a2c56ded688c8c
5
5
  SHA512:
6
- metadata.gz: 3f0b129f291c538614a488c47f3ce71971f24060b60719203df43a2a19cc82309141cc846c867b644bb54832e9bbee2390fee04c2bb1db7bcec0c1f200d3eecb
7
- data.tar.gz: 2d5e307edb7b5fdc1fbb0f1c0a1c830c6489fd8c7a11b3c08827fb3df9c49e7f1e23493a9165f23a5b2cbd7cd465913486fe174c79b7668731b7e5c7359e3e49
6
+ metadata.gz: f0af4e104fc9cc2b0a99f0ebd4a996994f16500302e5f1cabf635569de8d404c30bf289ce4df145376fad0579b5cf1dc865cee19fd45e757954aa0ebcd4c75bc
7
+ data.tar.gz: 67dd60f0a67b57a2cd09605a7aec9b91a822d198ed95c3be94d67663f6aef5b4db0d4b024ef09bd97e764396381d12c0f0b413552efde25f87fac44d57b48d88
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.1
1
+ 3.4.12
data/README.md CHANGED
@@ -67,6 +67,7 @@ Every generated project comes with:
67
67
  - **Example tests or a skeleton (`examples:false`)** — start from the working login examples, or pass `examples:false` for an empty skeleton with the helpers, config, CI and README (see [Getting Started](#getting-started))
68
68
  - **Login reuse** — generated web projects (Selenium or Watir) log in once and restore the saved session for examples tagged `:logged_in` (RSpec) or `@logged_in` (Cucumber); `RAIDER_LOGIN_STATE=off|refresh` and `login_state: max_age:` control it, and the state stays in git-ignored `.raider/auth/`
69
69
  - **Failure video replay** — Every test is recorded and the video is kept only for failing tests, attached to the report as `video` (WebM from page screenshots for Selenium and Watir, Appium's own MP4 for mobile, which needs `ffmpeg` on the Appium server host for iOS and for Android recordings over 180 s). Turn it off per run with `VIDEO=off`, or per project with `raider u video off`; ffmpeg is downloaded once, SHA-256 checked and cached in `~/.cache/raider`. The RaiderHQ `ffmpeg-7.1` build that the download uses is not published yet, so for now set `RAIDER_FFMPEG_PATH` to an ffmpeg with libvpx to get web videos
70
+ - **Failure trace replay** (Selenium with RSpec or Cucumber) — Failing tests keep a step-by-step trace: each action with its locator and wait time, a screenshot and the page HTML after every step that changes the page, plus network requests and console messages on Chrome, Edge and Firefox. It is saved as `raider-results/attachments/<n>-trace.zip`, attached to the report as `trace` and opened in Raider Desktop or on Hideout. `TRACE=on` keeps passing tests too, `TRACE=off` captures nothing, and `raider u trace on|off` sets it for the project. Typed values in password fields and `RAIDER_SECRET_*` values are hidden; other typed values, page text and URLs are kept, so treat a trace like the video. Request headers, cookies and bodies are never captured
70
71
  - **GitHub Actions CI** — Ready-to-use test pipeline that caches ffmpeg for failure videos
71
72
  - **Rake tasks** — Default, smoke, and regression tasks with tag-based filtering
72
73
  - **Retry support** — RSpec retry with configurable retry count via `RETRY_COUNT` env var
@@ -204,6 +205,7 @@ raider u platform [PLATFORM] # Set platform for cross-platform tests
204
205
  raider u start_appium # Start Appium server
205
206
  raider u desktop # Download Raider Desktop
206
207
  raider u video [ACTION] # Failure videos: install (ffmpeg), status, on, off
208
+ raider u trace [on/off] # Failure traces: turn on or off (Selenium)
207
209
  ```
208
210
 
209
211
  ## Development
@@ -123,7 +123,23 @@ class UtilityCommands < Thor
123
123
  end
124
124
  end
125
125
 
126
+ desc 'trace [on|off]', 'Turns trace capture on or off (writes trace.enabled in config/config.yml)'
127
+
128
+ def trace(action)
129
+ case action
130
+ when 'on', 'off' then trace_toggle(action == 'on')
131
+ else
132
+ say_error "Unknown trace command '#{action}'. Use on|off", :red
133
+ exit 1
134
+ end
135
+ end
136
+
126
137
  no_commands do
138
+ def trace_toggle(enabled)
139
+ Utilities.trace_enabled = enabled
140
+ say "Trace capture #{enabled ? 'enabled' : 'disabled'}", :green
141
+ end
142
+
127
143
  def video_install
128
144
  require_relative '../ruby_raider/ffmpeg'
129
145
  path = RubyRaider::Ffmpeg.ensure!
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <%- if web? && examples? -%>
2
4
  require_relative '../components/header'
3
5
  <%- end -%>
@@ -12,9 +14,12 @@ class Page
12
14
  <%- if wait_helper? -%>
13
15
  include WaitHelper
14
16
  <%- end -%>
15
- <%=- partial('initialize_selector', trim: false) -%>
16
- <%=- partial('visit_method', trim: false) -%>
17
- <%=- partial('url_methods', trim: false) -%>
17
+ <%- if cross_platform? || wait_helper? -%>
18
+
19
+ <%- end -%>
20
+ <%= partial('initialize_selector') -%>
21
+ <%= partial('visit_method') -%>
22
+ <%= partial('url_methods') -%>
18
23
 
19
24
  def to_s
20
25
  self.class.to_s.sub('Page', ' Page')
@@ -1,13 +1,13 @@
1
- <% if watir? %>
1
+ <%- if watir? -%>
2
2
  attr_reader :browser
3
3
 
4
4
  def initialize(browser)
5
5
  @browser = browser
6
6
  end
7
- <% else %>
7
+ <%- else -%>
8
8
  attr_reader :driver
9
9
 
10
10
  def initialize(driver)
11
11
  @driver = driver
12
12
  end
13
- <% end %>
13
+ <%- end -%>
@@ -1,4 +1,5 @@
1
- <% if web? %>
1
+ <%- if web? -%>
2
+
2
3
  def full_url(*page)
3
4
  "#{base_url}#{url(*page)}"
4
5
  end
@@ -10,4 +11,4 @@
10
11
  def url(_page)
11
12
  raise 'Url must be defined on child pages'
12
13
  end
13
- <% end %>
14
+ <%- end -%>
@@ -1,11 +1,11 @@
1
- <% if web? %>
2
- <% if selenium_based? %>
1
+ <%- if selenium_based? -%>
2
+
3
3
  def visit(*page)
4
4
  @driver.navigate.to full_url(page.first)
5
5
  end
6
- <% else %>
6
+ <%- elsif watir? -%>
7
+
7
8
  def visit(*page)
8
9
  @browser.goto full_url(page.first)
9
10
  end
10
- <% end %>
11
- <% end %>
11
+ <%- end -%>
@@ -53,6 +53,6 @@ class CucumberGenerator < Generator
53
53
  end
54
54
 
55
55
  def template_name
56
- @template_name ||= (@_initializer.first & %w[android ios cross_platform]).empty? ? 'login' : 'home'
56
+ @template_name ||= mobile? ? 'home' : 'login'
57
57
  end
58
58
  end
@@ -1,7 +1,7 @@
1
1
  <%- if selenium_based? -%>
2
- <%= partial('selenium_env') -%>
2
+ <%= partial('selenium_env') -%>
3
3
  <%- elsif mobile? -%>
4
- <%= partial('appium_env') -%>
4
+ <%= partial('appium_env') -%>
5
5
  <%- else -%>
6
- <%= partial('watir_env') -%>
6
+ <%= partial('watir_env') -%>
7
7
  <%- end -%>
@@ -8,6 +8,9 @@ require 'raider_reporter/cucumber'
8
8
  require 'tmpdir'
9
9
  require 'yaml'
10
10
  require_relative '../../helpers/video_helper'
11
+ <%- if trace_helper? -%>
12
+ require_relative '../../helpers/trace_helper'
13
+ <%- end -%>
11
14
  require_relative '../../helpers/session_state'
12
15
  <%- if examples? -%>
13
16
  require_relative '../../models/user_factory'
@@ -15,7 +18,47 @@ require_relative '../../page_objects/pages/account'
15
18
  require_relative '../../page_objects/pages/login'
16
19
  <%- end -%>
17
20
 
18
- Before do
21
+ <%- if trace_helper? -%>
22
+ # Attempts per scenario across --retry, keyed on the scenario's location, for the trace of each attempt.
23
+ TRACE_ATTEMPTS = Hash.new(0) unless defined?(TRACE_ATTEMPTS)
24
+
25
+ # Each Gherkin step becomes a group in the trace, named by its keyword and text; hook steps are not steps.
26
+ InstallPlugin do |config, _registry|
27
+ if defined?(TraceHelper)
28
+ begin
29
+ ast_lookup = Cucumber::Formatter::AstLookup.new(config)
30
+ groups = {}
31
+ config.on_event(:test_step_started) do |event|
32
+ next if event.test_step.hook?
33
+
34
+ step = event.test_step
35
+ keyword = begin
36
+ ast_lookup.step_source(step).step.keyword.strip
37
+ rescue StandardError
38
+ nil
39
+ end
40
+ location = { file: step.location.file, line: step.location.lines.min }
41
+ groups[step.id] = TraceHelper.active_recorder&.step_started(name: [keyword, step.text].compact.join(' '), source: location)
42
+ rescue StandardError => e
43
+ TraceHelper.disable!("step groups failed: #{e.message}") if defined?(TraceHelper)
44
+ end
45
+ config.on_event(:test_step_finished) do |event|
46
+ next if event.test_step.hook?
47
+
48
+ status = %i[passed failed].include?(event.result.to_sym) ? event.result.to_sym : :skipped
49
+ error = event.result.failed? ? event.result.exception : nil
50
+ TraceHelper.active_recorder&.step_finished(groups.delete(event.test_step.id), status: status, error: error)
51
+ rescue StandardError => e
52
+ TraceHelper.disable!("step groups failed: #{e.message}") if defined?(TraceHelper)
53
+ end
54
+ rescue StandardError => e
55
+ TraceHelper.warn_once("Gherkin step groups unavailable: #{e.message}")
56
+ end
57
+ end
58
+ end
59
+
60
+ <%- end -%>
61
+ Before do<%= trace_helper? ? ' |scenario|' : '' %>
19
62
  viewport = YAML.load_file('config/config.yml')['viewport']
20
63
  if viewport
21
64
  driver.manage.window.resize_to(viewport['width'], viewport['height'])
@@ -24,6 +67,14 @@ Before do
24
67
  end
25
68
  @video_recorder = VideoHelper.recorder_for(driver)
26
69
  @video_recorder.start
70
+ <%- if trace_helper? -%>
71
+ if defined?(TraceHelper)
72
+ @trace_attempt = (TRACE_ATTEMPTS[scenario.location.to_s] += 1)
73
+ @trace_test = { identifier: scenario.location.to_s, full_name: scenario.name, attempt: @trace_attempt }
74
+ @trace_recorder = TraceHelper.recorder_for(driver)
75
+ @trace_recorder.start
76
+ end
77
+ <%- end -%>
27
78
  end
28
79
 
29
80
  <%- if examples? -%>
@@ -41,8 +92,18 @@ end
41
92
 
42
93
  <%- end -%>
43
94
  After do |scenario|
95
+ <%- if trace_helper? -%>
96
+ if defined?(TraceHelper)
97
+ keep_trace = TraceHelper.keep_all? || scenario.failed?
98
+ trace_test = @trace_test&.merge(status: scenario.failed? ? 'failed' : 'passed', error: scenario.exception)
99
+ trace_path = @trace_recorder&.stop(keep: keep_trace, test: trace_test)
100
+ end
101
+ <%- end -%>
44
102
  video_path = @video_recorder&.stop(keep: scenario.failed?)
45
103
  RaiderReporter.attach('video', video_path, type: :webm) if video_path
104
+ <%- if trace_helper? -%>
105
+ RaiderReporter.attach('trace', trace_path, type: :trace, attempt: @trace_attempt) if trace_path
106
+ <%- end -%>
46
107
  if scenario.failed?
47
108
  Dir.mktmpdir do |temp_folder|
48
109
  screenshot_path = File.join(temp_folder, 'screenshot.png')
@@ -51,6 +112,9 @@ After do |scenario|
51
112
  end
52
113
  end
53
114
  ensure
115
+ <%- if trace_helper? -%>
116
+ @trace_recorder&.cleanup
117
+ <%- end -%>
54
118
  @video_recorder&.cleanup
55
119
  driver.quit
56
120
  end
@@ -1,5 +1,5 @@
1
1
  <%- if web? -%>
2
- <%= partial('web_steps', strip: true) -%>
2
+ <%= partial('web_steps', strip: true) %>
3
3
  <%- else -%>
4
- <%= partial('mobile_steps', strip: true) -%>
4
+ <%= partial('mobile_steps', strip: true) %>
5
5
  <%- end -%>
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'model_factory'
2
4
 
3
5
  class UserFactory < ModelFactory
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <%- if watir? -%>
2
- <%=- partial('watir_world', strip: true) -%>
4
+ <%= partial('watir_world', strip: true) %>
3
5
  <%- else -%>
4
- <%=- partial('driver_world', strip: true) -%>
5
- <%- end -%>
6
+ <%= partial('driver_world', strip: true) %>
7
+ <%- end -%>
@@ -23,37 +23,36 @@ class Generator < Thor::Group
23
23
  end
24
24
 
25
25
  def cucumber?
26
- args.include?('cucumber')
26
+ framework == 'cucumber'
27
27
  end
28
28
 
29
29
  # The framework is cross platform when it supports Android and iOS
30
30
  def cross_platform?
31
- args.include?('cross_platform')
31
+ automation == 'cross_platform'
32
32
  end
33
33
 
34
34
  def mobile?
35
- (args & %w[android ios cross_platform]).count.positive?
35
+ %w[android ios cross_platform].include?(automation)
36
36
  end
37
37
 
38
38
  def single_platform?
39
- (args & %w[android ios]).count.positive?
39
+ %w[android ios].include?(automation)
40
40
  end
41
41
 
42
42
  def ios?
43
- args.include?('ios')
43
+ automation == 'ios'
44
44
  end
45
45
 
46
46
  def android?
47
- args.include?('android')
47
+ automation == 'android'
48
48
  end
49
49
 
50
50
  def rspec?
51
- args.include?('rspec')
51
+ framework == 'rspec'
52
52
  end
53
53
 
54
- # Only the options after automation, framework and name, so a project named "azure" is not Azure CI
55
54
  def azure?
56
- args.drop(3).include?('azure')
55
+ options_after_name.include?('azure')
57
56
  end
58
57
 
59
58
  def github_ci?
@@ -74,9 +73,14 @@ class Generator < Thor::Group
74
73
  !watir?
75
74
  end
76
75
 
76
+ # Selenium web projects record a trace per test; it needs WaitHelper's actions, and mobile has none yet
77
+ def trace_helper?
78
+ wait_helper? && !mobile?
79
+ end
80
+
77
81
  # False for `raider new -p examples:false`: an empty skeleton without the example pages, tests and wiring
78
82
  def examples?
79
- !args.drop(3).include?('no_examples')
83
+ !options_after_name.include?('no_examples')
80
84
  end
81
85
 
82
86
  def web?
@@ -84,20 +88,29 @@ class Generator < Thor::Group
84
88
  end
85
89
 
86
90
  def axe_addon?
87
- args.include?('axe_addon')
91
+ options_after_name.include?('axe_addon')
88
92
  end
89
93
 
90
94
  def selenium_based?
91
95
  selenium?
92
96
  end
93
97
 
98
+ def ruby_raider_version
99
+ File.read(File.expand_path('../version', __dir__)).strip
100
+ end
101
+
94
102
  def ruby_raider_version_requirement
95
- major, minor = File.read(File.expand_path('../version', __dir__)).strip.split('.')
103
+ major, minor = ruby_raider_version.split('.')
96
104
  "~> #{major}.#{minor}"
97
105
  end
98
106
 
99
107
  private
100
108
 
109
+ # Option flags only: everything after automation, framework and name
110
+ def options_after_name
111
+ args.drop(3)
112
+ end
113
+
101
114
  def _initializer
102
115
  @_initializer ||= super
103
116
  end
@@ -11,6 +11,7 @@ class HelpersGenerator < Generator
11
11
  generate_video_helper
12
12
  generate_session_state
13
13
  generate_wait_helper
14
+ generate_trace_helper
14
15
  end
15
16
 
16
17
  private
@@ -50,6 +51,12 @@ class HelpersGenerator < Generator
50
51
  template('helpers/raider_element.tt', "#{name}/helpers/raider_element.rb")
51
52
  end
52
53
 
54
+ def generate_trace_helper
55
+ return unless trace_helper?
56
+
57
+ template('helpers/trace_helper.tt', "#{name}/helpers/trace_helper.rb")
58
+ end
59
+
53
60
  def generate_appium_helper
54
61
  return unless cross_platform?
55
62
 
@@ -4,18 +4,18 @@ require_relative '../helpers/spec_helper'
4
4
 
5
5
  describe 'Accessibility' do
6
6
  before do
7
- <% if watir? -%>
8
- browser.goto 'https://raider-test-site.onrender.com/'
9
- <% else -%>
10
- driver.get 'https://raider-test-site.onrender.com/'
11
- <% end -%>
7
+ <%- if watir? -%>
8
+ browser.goto 'https://raider-test-site.onrender.com/'
9
+ <%- else -%>
10
+ driver.get 'https://raider-test-site.onrender.com/'
11
+ <%- end -%>
12
12
  end
13
13
 
14
14
  it 'has no accessibility violations on the page' do
15
- <% if watir? -%>
16
- expect(browser.driver).to be_axe_clean
17
- <% else -%>
18
- expect(driver).to be_axe_clean
19
- <% end -%>
15
+ <%- if watir? -%>
16
+ expect(browser.driver).to be_axe_clean
17
+ <%- else -%>
18
+ expect(driver).to be_axe_clean
19
+ <%- end -%>
20
20
  end
21
21
  end
@@ -63,6 +63,8 @@ describe 'Account', :logged_in do
63
63
  end
64
64
  # rubocop:enable RSpec/MultipleDescribes
65
65
  <%- else -%>
66
+ # frozen_string_literal: true
67
+
66
68
  <% if cross_platform? -%>
67
69
  require_relative '../helpers/appium_helper'
68
70
  <%- end -%>
@@ -72,7 +74,6 @@ require_relative '../page_objects/pages/pdp'
72
74
 
73
75
  class PdpSpec
74
76
  describe 'PDP' do
75
-
76
77
  let(:home_page) { Home.new(driver) }
77
78
  let(:pdp_page) { Pdp.new(driver) }
78
79
 
@@ -1,32 +1,32 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'activesupport'
4
- <% if mobile? %>
5
- gem 'appium_lib'
4
+ <%- if mobile? -%>
6
5
  gem 'appium_console'
7
- <% end %>
6
+ gem 'appium_lib'
7
+ <%- end -%>
8
8
  <%- if axe_addon? -%>
9
9
  gem 'axe-core-rspec'
10
10
  gem 'axe-core-selenium'
11
11
  <%- end -%>
12
+ <%- if cucumber? -%>
13
+ gem 'cucumber'
14
+ <%- end -%>
12
15
  gem 'parallel_split_test'
13
16
  gem 'parallel_tests'
17
+ gem 'raider_reporter', '~> 0.12'
14
18
  gem 'rake'
15
19
  gem 'reek'
16
- gem 'raider_reporter', '~> 0.10'
20
+ gem 'rspec'
17
21
  <%- if rspec? -%>
18
22
  gem 'rspec-retry'
19
23
  <%- end -%>
20
- gem '<%= framework %>'
21
- <%- if cucumber? -%>
22
- gem 'rspec'
23
- <%- end -%>
24
24
  gem 'rubocop'
25
25
  <%- if rspec? -%>
26
26
  gem 'rubocop-rspec'
27
27
  <%- end -%>
28
28
  gem 'ruby_raider', '<%= ruby_raider_version_requirement %>'
29
- <%- if selenium_based? %>
29
+ <%- if selenium_based? -%>
30
30
  gem 'selenium-webdriver'
31
31
  <%- end -%>
32
32
  <%- if watir? -%>
@@ -19,6 +19,13 @@ video:
19
19
  enabled: true
20
20
  fps: 5
21
21
  max_width: 1280
22
+ <%- if trace_helper? -%>
23
+
24
+ trace:
25
+ enabled: true # capture traces; failing tests keep theirs
26
+ max_steps: 200 # newest steps are kept; older ones lose screenshots and snapshots first
27
+ max_mb: 20 # the zip stays under this; see Cost
28
+ <%- end -%>
22
29
 
23
30
  login_state:
24
31
  max_age: 3600 # seconds a saved login is reused before logging in again
@@ -224,6 +224,9 @@ timeout: 15
224
224
  - `headless`: not in the file by default. Add `headless: true`, or set `HEADLESS=true` for one run, to run the
225
225
  browser without a window.
226
226
  - `video`: failure videos, see [Video replay](#video-replay).
227
+ <%- if trace_helper? -%>
228
+ - `trace`: failing-test traces, see [Trace replay](#trace-replay).
229
+ <%- end -%>
227
230
  - `login_state`: how long a saved login is reused, see [Reusing the login](#reusing-the-login).
228
231
  <%- end -%>
229
232
 
@@ -530,6 +533,10 @@ The wait helper is ordinary code in your project, so you can take it out. To do
530
533
  <%- end -%>
531
534
  5. Scaffolding (`raider g page` and `raider g component`) then writes plain `driver.find_element` examples on its
532
535
  own, because it checks for `helpers/wait_helper.rb`.
536
+ <%- if trace_helper? -%>
537
+
538
+ Note that without WaitHelper a trace records only navigation and the failure (see [Trace replay](#trace-replay)).
539
+ <%- end -%>
533
540
 
534
541
  <%- end -%>
535
542
  <%- if web? -%>
@@ -674,6 +681,44 @@ The video is a `video.webm` built from page screenshots with ffmpeg.
674
681
  no video.
675
682
  <%- end -%>
676
683
 
684
+ <%- if trace_helper? -%>
685
+ ### Trace replay
686
+
687
+ Every test records a trace, and the trace is kept only for failing tests<%= rspec? ? ' (on the last retry attempt)' : '' %>. A trace is a step-by-step
688
+ record of the test: each action and page load, with the locator, how long it waited, a screenshot and the page HTML
689
+ after it, the network requests, and the browser console. It is saved as `raider-results/attachments/<n>-trace.zip`,
690
+ next to the failure screenshot and video, and attached to the report as `trace`.
691
+
692
+ - Keep the trace of passing tests too with `TRACE=on bundle exec <%= rspec? ? 'rspec' : 'cucumber' %>`. Capture nothing for one run with
693
+ `TRACE=off bundle exec <%= rspec? ? 'rspec' : 'cucumber' %>`, or for the project with `raider u trace off` (`raider u trace on` turns it back on).
694
+ `raider u trace on` and `raider u trace off` write `enabled:` under `trace:` in `config/config.yml`.
695
+ - Open a trace in Raider Desktop, or from the test's page on Hideout when the run is uploaded.
696
+ - Cost: a trace takes one screenshot and one page HTML for every step that changes the page (a click, typing,
697
+ a page load), including in passing tests, whose traces are then thrown away. If that slows your suite down,
698
+ switch tracing off as above.
699
+ - Caps, under `trace:` in `config/config.yml`: `max_steps` (200) keeps the newest steps, and `max_mb` (20) keeps the
700
+ zip under that size. When a cap is reached, the HTML of the oldest steps is dropped first, then their
701
+ screenshots, then the oldest network and console entries. The last 10 steps always keep theirs.
702
+ - Network requests and console messages are captured only on Chrome, Edge or Firefox. Other browsers, such as
703
+ Safari, get a trace with steps, screenshots and HTML only. Capture is passive: the trace listens to the browser and
704
+ never changes a request.
705
+ - The trace helper never fails a test. When it has to leave something out, such as network on a browser without
706
+ BiDi or a screenshot it could not take, you get at most one `[trace]` notice on stderr for the run. If capturing
707
+ breaks, you get one more `[trace]` line ending in "tracing disabled for this run", and no more traces for that run.
708
+
709
+ **Treat a trace like the video: it can contain secrets.** What is hidden: values typed into password fields
710
+ (`type=password`, or an `autocomplete` that contains "password"), and any typed value equal to a `RAIDER_SECRET_*`
711
+ environment variable. Everything else is kept: other typed values, page text, URLs with their query strings, and
712
+ console text. So a secret typed into an ordinary field, or shown on the page, ends up in the trace. Failure messages
713
+ can contain typed text too, because an assertion message is kept as it is. Request headers, cookies and request or
714
+ response bodies are never captured. Do not share or upload traces from runs that handle real credentials.
715
+
716
+ **Removing the trace helper.** The trace helper is ordinary code in your project. Delete `helpers/trace_helper.rb` and
717
+ the `require_relative` lines that load `trace_helper`: in <%= rspec? ? '`helpers/driver_helper.rb`' : '`helpers/driver_helper.rb` and `features/support/env.rb`' %>. The other lines that use it
718
+ (in <%= rspec? ? '`helpers/spec_helper.rb`' : '`features/support/env.rb`' %> and the driver helper's listeners) only run when `TraceHelper` is defined,
719
+ so they can stay. Remove the `trace:` block from `config/config.yml` if you like.
720
+
721
+ <%- end -%>
677
722
  <%- unless azure? && mobile? -%>
678
723
  ### Quarantine
679
724
 
@@ -1,27 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AppiumHelper
2
4
  def element(opts = {})
3
- return driver.find_element(strategy(opts) => selector(opts)) if opts[os]
5
+ return driver.find_element(strategy(opts) => selector(opts)) if opts[os]
4
6
 
5
- driver.find_element(opts)
7
+ driver.find_element(opts)
6
8
  end
7
9
 
8
10
  def elements(opts = {})
9
- return driver.find_elements(strategy(opts) => selector(opts)) if opts[os]
11
+ return driver.find_elements(strategy(opts) => selector(opts)) if opts[os]
10
12
 
11
- driver.find_elements(opts)
12
- end
13
+ driver.find_elements(opts)
14
+ end
13
15
 
14
- def os
15
- driver.appium_device
16
- end
16
+ def os
17
+ driver.appium_device
18
+ end
17
19
 
18
- private
20
+ private
19
21
 
20
- def strategy(opts)
21
- opts[os].keys.first
22
- end
22
+ def strategy(opts)
23
+ opts[os].keys.first
24
+ end
23
25
 
24
- def selector(opts)
25
- opts[os][strategy(opts)]
26
- end
26
+ def selector(opts)
27
+ opts[os][strategy(opts)]
28
+ end
27
29
  end
@@ -6,7 +6,6 @@ require 'watir'
6
6
  require_relative 'video_helper'
7
7
 
8
8
  module BrowserHelper
9
-
10
9
  def browser(*args)
11
10
  @browser ||= create_browser(*args)
12
11
  end
@@ -23,7 +22,7 @@ module BrowserHelper
23
22
  args = args.empty? ? config['browser_arguments'][browser_name] : args
24
23
  args += ['--headless'] if (ENV['HEADLESS'] || config['headless']) && !args.include?('--headless')
25
24
  capitalized = browser_name == 'ie' ? browser_name.upcase : browser_name.capitalize
26
- browser_options = "Selenium::WebDriver::#{capitalized}::Options".constantize.new(args:)
25
+ browser_options = "Selenium::WebDriver::#{capitalized}::Options".constantize.new(args: args)
27
26
  arguments = { options: browser_options }
28
27
  listener = VideoHelper.listener
29
28
  arguments[:listener] = listener if listener