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
@@ -59,6 +59,22 @@ describe 'Config file content' do
59
59
  end
60
60
  end
61
61
 
62
+ shared_examples 'trace config' do |project_name|
63
+ it 'has tracing enabled with 200 steps and 20 MB, with the spec comments' do
64
+ raw = read_generated(project_name, 'config/config.yml')
65
+ config = YAML.safe_load(raw, permitted_classes: [Symbol])
66
+ expect(config['trace']).to eq('enabled' => true, 'max_steps' => 200, 'max_mb' => 20)
67
+ expect(raw.index('trace:')).to be > raw.index('video:')
68
+ expect(raw.index('trace:')).to be < raw.index('login_state:')
69
+ end
70
+ end
71
+
72
+ shared_examples 'no trace config' do |project_name|
73
+ it 'has no trace block' do
74
+ expect(read_generated(project_name, 'config/config.yml')).not_to include('trace')
75
+ end
76
+ end
77
+
62
78
  shared_examples 'login state config' do |project_name|
63
79
  it 'has a login_state max_age of an hour' do
64
80
  config = YAML.safe_load(read_generated(project_name, 'config/config.yml'), permitted_classes: [Symbol])
@@ -73,6 +89,7 @@ describe 'Config file content' do
73
89
  include_examples 'valid web config', name
74
90
  include_examples 'selenium config with timeout', name
75
91
  include_examples 'video config', name
92
+ include_examples 'trace config', name
76
93
  include_examples 'login state config', name
77
94
  end
78
95
 
@@ -80,6 +97,7 @@ describe 'Config file content' do
80
97
  name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
81
98
  include_examples 'valid web config', name
82
99
  include_examples 'config without driver_options', name
100
+ include_examples 'no trace config', name
83
101
  include_examples 'login state config', name
84
102
  end
85
103
 
@@ -87,6 +105,7 @@ describe 'Config file content' do
87
105
  name = "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
88
106
  include_examples 'valid web config', name
89
107
  include_examples 'selenium config with timeout', name
108
+ include_examples 'trace config', name
90
109
  include_examples 'login state config', name
91
110
  end
92
111
 
@@ -95,6 +114,7 @@ describe 'Config file content' do
95
114
  # Single-platform Appium projects have no config.yml; VideoHelper falls back to its defaults.
96
115
  context 'with rspec and cross platform' do
97
116
  include_examples 'video config', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
117
+ include_examples 'no trace config', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
98
118
  end
99
119
 
100
120
  context 'with rspec and appium android' do
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../spec_helper'
4
+ require_relative '../frozen_string_literal_matcher'
4
5
 
5
6
  module ContentHelper
6
7
  def read_generated(project, relative_path)
@@ -12,11 +13,6 @@ module ContentHelper
12
13
  end
13
14
  end
14
15
 
15
- RSpec::Matchers.define :have_frozen_string_literal do
16
- match { |content| content.include?('# frozen_string_literal: true') }
17
- failure_message { 'expected file to contain frozen_string_literal magic comment' }
18
- end
19
-
20
16
  RSpec::Matchers.define :have_valid_ruby_syntax do
21
17
  match do |content|
22
18
  RubyVM::InstructionSequence.compile(content)
@@ -68,6 +68,57 @@ describe 'Cucumber raider_reporter wiring' do
68
68
  end
69
69
  end
70
70
 
71
+ if automation == 'selenium'
72
+ context 'with trace recording' do
73
+ it 'requires trace_helper' do
74
+ expect(env).to include("require_relative '../../helpers/trace_helper'")
75
+ end
76
+
77
+ it 'counts attempts per scenario location and starts the trace recorder after the video recorder' do
78
+ before_hook = env[/^Before do.*?^end$/m]
79
+ expect(env).to match(/^TRACE_ATTEMPTS = Hash\.new\(0\) unless defined\?\(TRACE_ATTEMPTS\)$/)
80
+ expect(before_hook).to include('TRACE_ATTEMPTS[scenario.location.to_s] += 1', '@trace_recorder = TraceHelper.recorder_for(driver)')
81
+ expect(before_hook).to match(/^Before do \|scenario\|$/)
82
+ expect(before_hook.index('@video_recorder.start')).to be < before_hook.index('@trace_recorder = TraceHelper')
83
+ expect(before_hook).to match(/if defined\?\(TraceHelper\)\n(.*\n)*.*@trace_recorder = TraceHelper/)
84
+ end
85
+
86
+ it 'stops with keep: TraceHelper.keep_all? || scenario.failed? before the video, attaches it and cleans up before quitting' do
87
+ expect(hook).to include('TraceHelper.keep_all? || scenario.failed?', 'error: scenario.exception', 'attempt: @trace_attempt')
88
+ expect(hook).to include("RaiderReporter.attach('trace', trace_path, type: :trace, attempt: @trace_attempt) if trace_path")
89
+ order = [hook.index('@trace_recorder&.stop'), hook.index('@video_recorder&.stop'), hook.index("RaiderReporter.attach('trace'"),
90
+ hook.index(/^ensure/), hook.index('@trace_recorder&.cleanup'), hook.index('@video_recorder&.cleanup'), hook.index('driver.quit')]
91
+ expect(order).to all(be_an(Integer)).and eq(order.compact.sort)
92
+ end
93
+
94
+ it 'survives a Before that raised before the trace init, and a failing plugin setup or handler' do
95
+ expect(hook).to include('@trace_test&.merge(')
96
+ expect(hook).not_to include('@trace_test.merge(')
97
+ plugin = env[/^InstallPlugin do.*?^end$/m]
98
+ expect(plugin.scan('rescue StandardError => e').size).to eq(3)
99
+ expect(plugin.index('AstLookup.new')).to be > plugin.index("begin\n")
100
+ expect(plugin).to include('TraceHelper.warn_once("Gherkin step groups unavailable')
101
+ end
102
+
103
+ it 'turns tracing off with TraceHelper.disable! when a step group handler raises' do
104
+ plugin = env[/^InstallPlugin do.*?^end$/m]
105
+ expect(plugin.scan(/TraceHelper\.disable!\("step groups failed: \#\{e\.message\}"\) if defined\?\(TraceHelper\)/).size).to eq(2)
106
+ expect(plugin).not_to include('tracing disabled')
107
+ end
108
+
109
+ it 'records each Gherkin step as a group through the InstallPlugin step events, skipping hooks' do
110
+ plugin = env[/^InstallPlugin do.*?^end$/m]
111
+ expect(plugin).to include('config.on_event(:test_step_started)', 'config.on_event(:test_step_finished)', 'next if event.test_step.hook?')
112
+ expect(plugin).to include('TraceHelper.active_recorder&.step_started', 'TraceHelper.active_recorder&.step_finished')
113
+ expect(plugin).to include('defined?(TraceHelper)')
114
+ end
115
+ end
116
+ else
117
+ it 'has no trace wiring' do
118
+ expect(env).not_to match(/trace/i)
119
+ end
120
+ end
121
+
71
122
  it 'does not generate the allure helper' do
72
123
  expect(File).not_to exist(File.join(project_name, 'helpers/allure_helper.rb'))
73
124
  end
@@ -127,7 +127,7 @@ describe 'Gemfile content' do
127
127
  let(:gemfile) { read_generated("#{FrameworkIndex::RSPEC}_#{automation}", 'Gemfile') }
128
128
 
129
129
  it 'includes raider_reporter' do
130
- expect(gemfile).to include("gem 'raider_reporter', '~> 0.10'")
130
+ expect(gemfile).to include("gem 'raider_reporter', '~> 0.12'")
131
131
  end
132
132
 
133
133
  it 'has no allure gems' do
@@ -143,7 +143,7 @@ describe 'Gemfile content' do
143
143
  let(:gemfile) { read_generated("#{FrameworkIndex::CUCUMBER}_#{automation}", 'Gemfile') }
144
144
 
145
145
  it 'includes raider_reporter' do
146
- expect(gemfile).to include("gem 'raider_reporter', '~> 0.10'")
146
+ expect(gemfile).to include("gem 'raider_reporter', '~> 0.12'")
147
147
  end
148
148
 
149
149
  it 'drops allure-cucumber' do
@@ -43,6 +43,21 @@ describe 'Helper file content' do
43
43
  it 'does not reference Capybara' do
44
44
  expect(helper).not_to include('Capybara')
45
45
  end
46
+
47
+ it 'requires trace_helper' do
48
+ expect(helper).to include("require_relative 'trace_helper'")
49
+ end
50
+
51
+ it 'sets web_socket_url only for chrome, edge and firefox and not with TRACE=off' do
52
+ expect(helper).to include('options.web_socket_url = true if defined?(TraceHelper) && TraceHelper.bidi?(load_browser)')
53
+ end
54
+
55
+ it 'fans the video and trace listeners out through one listener, each guarded by defined?' do
56
+ expect(helper).to include('class FanOutListener < Selenium::WebDriver::Support::AbstractEventListener',
57
+ 'listener = FanOutListener.for(listeners)',
58
+ 'listeners << VideoHelper.listener if defined?(VideoHelper)',
59
+ 'listeners << TraceHelper.listener if defined?(TraceHelper)')
60
+ end
46
61
  end
47
62
 
48
63
  # --- Browser helper ---
@@ -168,7 +183,7 @@ describe 'Helper file content' do
168
183
  after_hook.index(/driver\.quit|browser\.quit/)]
169
184
  expect(order).to all(be_an(Integer)).and eq(order.compact.sort)
170
185
  expect(after_hook.scan(/driver\.quit|browser\.quit/).size).to eq(1)
171
- expect(after_hook[order[2]..]).to match(/\n @video_recorder&\.cleanup\n (driver|browser)\.quit(_driver)?\n end\n/)
186
+ expect(after_hook[order[2]..]).to match(/\n (@trace_recorder&\.cleanup\n )?@video_recorder&\.cleanup\n (driver|browser)\.quit(_driver)?\n end\n/)
172
187
  end
173
188
 
174
189
  it 'keeps the screenshot attachment' do
@@ -176,6 +191,41 @@ describe 'Helper file content' do
176
191
  end
177
192
  end
178
193
 
194
+ shared_examples 'trace spec helper wiring' do |project_name|
195
+ subject(:helper) { read_generated(project_name, 'helpers/spec_helper.rb') }
196
+
197
+ let(:before_hook) { helper[/config\.before\(:each\) do.*?^ end$/m] }
198
+ let(:after_hook) { helper[/config\.after\(:each\).*\z/m] }
199
+
200
+ it 'starts the trace recorder after the video recorder, each guarded by defined?(TraceHelper)' do
201
+ expect(before_hook).to include('@trace_recorder = TraceHelper.recorder_for(driver) if defined?(TraceHelper)', '@trace_recorder&.start')
202
+ expect(before_hook.index('@video_recorder.start')).to be < before_hook.index('@trace_recorder = TraceHelper')
203
+ end
204
+
205
+ it 'keeps the trace for TRACE=on or a final-attempt failure, falling back to keeping failures without a video helper' do
206
+ expect(after_hook).to include('TraceHelper.keep_all? || (example.exception && (defined?(VideoHelper) ? VideoHelper.final_attempt?(example) : true))')
207
+ end
208
+
209
+ it 'counts the attempt from the rspec-retry metadata and hands the recorder the test' do
210
+ expect(after_hook).to include('example.metadata[:retry_attempts].to_i + 1', 'error: example.exception')
211
+ expect(after_hook).to include('@trace_recorder&.stop(keep: keep_trace, test: trace_test)')
212
+ end
213
+
214
+ it 'stops the trace before the video and the screenshot, then attaches it, then cleans up before quitting' do
215
+ order = [after_hook.index('@trace_recorder&.stop'), after_hook.index('@video_recorder&.stop'), after_hook.index('RaiderReporter.attach(\'screenshot\''),
216
+ after_hook.index("RaiderReporter.attach('trace', trace_path, type: :trace, attempt: attempt) if trace_path"),
217
+ after_hook.index("\n ensure\n"), after_hook.index('@trace_recorder&.cleanup'), after_hook.index('@video_recorder&.cleanup'),
218
+ after_hook.index('driver.quit')]
219
+ expect(order).to all(be_an(Integer)).and eq(order.compact.sort)
220
+ end
221
+ end
222
+
223
+ shared_examples 'no trace wiring' do |project_name, file|
224
+ it "leaves #{file} without trace code" do
225
+ expect(read_generated(project_name, file)).not_to match(/trace/i)
226
+ end
227
+ end
228
+
179
229
  shared_examples 'selenium spec helper' do |project_name|
180
230
  subject(:helper) { read_generated(project_name, 'helpers/spec_helper.rb') }
181
231
 
@@ -313,6 +363,10 @@ describe 'Helper file content' do
313
363
  it 'does not wire a listener into the appium driver helper' do
314
364
  expect(read_generated(name, 'helpers/driver_helper.rb')).not_to include('listener')
315
365
  end
366
+
367
+ it 'does not wire tracing or BiDi into the appium driver helper' do
368
+ expect(read_generated(name, 'helpers/driver_helper.rb')).not_to include('TraceHelper', 'web_socket_url')
369
+ end
316
370
  end
317
371
 
318
372
  # --- RSpec + automation contexts ---
@@ -357,6 +411,11 @@ describe 'Helper file content' do
357
411
  it_behaves_like 'raider reporter spec helper', name, screenshot_call
358
412
  it_behaves_like 'video spec helper wiring', name, automation == 'watir' ? 'browser' : 'driver',
359
413
  %w[selenium watir].include?(automation) ? ':webm' : ':mp4'
414
+ if automation == 'selenium'
415
+ it_behaves_like 'trace spec helper wiring', name
416
+ else
417
+ it_behaves_like 'no trace wiring', name, 'helpers/spec_helper.rb'
418
+ end
360
419
  end
361
420
 
362
421
  context "with cucumber and #{automation}" do
@@ -52,6 +52,52 @@ describe 'Generated Readme content' do
52
52
  end
53
53
  end
54
54
  end
55
+
56
+ context "with #{framework} and #{automation}, the Trace replay section" do
57
+ subject(:section) { readme[/^### Trace replay\n.*?(?=^##)/m]&.gsub(/\s+/, ' ') }
58
+
59
+ let(:readme) { read_generated("#{framework}_#{automation}", 'Readme.md') }
60
+
61
+ if automation == 'selenium'
62
+ it 'covers the defaults, TRACE=on, TRACE=off, the file location and how to open it' do
63
+ expect(section).to include('only for failing tests', 'TRACE=on', 'TRACE=off', 'raider u trace off',
64
+ 'raider u trace on', 'raider-results/attachments/', 'Raider Desktop', 'Hideout',
65
+ 'helpers/trace_helper.rb')
66
+ expect(section).to include('raider-results/attachments/<n>-trace.zip')
67
+ expect(section).to include('`raider u trace on` and `raider u trace off` write `enabled:`')
68
+ expect(section).not_to include('Both write')
69
+ end
70
+
71
+ it 'states the per-step cost and the caps' do
72
+ expect(section).to include('one screenshot and one page HTML', 'max_mb', 'max_steps', 'trace:')
73
+ end
74
+
75
+ it 'allows at most one [trace] notice, plus one line when tracing stops' do
76
+ expect(section).to include('at most one `[trace]` notice', 'one more `[trace]` line', 'tracing disabled for this run')
77
+ end
78
+
79
+ it 'limits network and console to Chrome, Edge or Firefox' do
80
+ expect(section).to include('Chrome, Edge or Firefox', 'Safari', 'steps, screenshots and HTML')
81
+ end
82
+
83
+ it 'says what is hidden, what is kept and what is never captured' do
84
+ expect(section).to include('password', 'RAIDER_SECRET_', 'page text', 'query strings', 'console text',
85
+ 'like the video', 'never captured', 'cookies')
86
+ expect(section).to include('Failure messages can contain typed text too, because an assertion message is kept as it is')
87
+ end
88
+
89
+ it 'says how to remove the trace helper and notes the wait helper link' do
90
+ expect(readme).to include('Removing the trace helper', 'require_relative',
91
+ 'without WaitHelper a trace records only navigation and the failure')
92
+ expect(readme).not_to match(/allure/i)
93
+ expect(readme).not_to include('—')
94
+ end
95
+ else
96
+ it 'does not mention trace replay' do
97
+ expect(readme).not_to include('Trace replay', 'TRACE=', 'trace_helper')
98
+ end
99
+ end
100
+ end
55
101
  end
56
102
 
57
103
  AUTOMATION_TYPES.each do |automation|
@@ -18,8 +18,8 @@ describe 'RSpec projects without Allure' do
18
18
  expect(File).not_to exist(File.join(project_name, 'allure-results'))
19
19
  end
20
20
 
21
- it 'pins raider_reporter 0.10' do
22
- expect(read_generated(project_name, 'Gemfile')).to include("gem 'raider_reporter', '~> 0.10'")
21
+ it 'pins raider_reporter 0.12' do
22
+ expect(read_generated(project_name, 'Gemfile')).to include("gem 'raider_reporter', '~> 0.12'")
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'rubocop'
5
+ require 'stringio'
6
+ require 'tmpdir'
7
+ require_relative 'content_helper'
8
+
9
+ RUBOCOP_WEB_AUTOMATIONS = %w[selenium watir].freeze
10
+
11
+ # Every generated project must pass the RuboCop it ships: its own .rubocop.yml, run in-process with
12
+ # this gem's bundle. The project is linted from a copy outside this repo, so RuboCop resolves the
13
+ # target Ruby the way a user's fresh project does (no TargetRubyVersion, so its 2.7 default) instead
14
+ # of picking up this repo's .ruby-version or gemspec. The end-to-end spec repeats the check with the
15
+ # project's own resolved gems.
16
+ describe 'Generated projects pass their own RuboCop' do
17
+ def rubocop_report(project_name)
18
+ Dir.mktmpdir('raider_rubocop') do |tmp|
19
+ dir = File.join(tmp, File.basename(project_name))
20
+ FileUtils.cp_r(project_name, dir)
21
+ with_project_root(dir) { yield lint(dir), target_ruby(dir) }
22
+ end
23
+ end
24
+
25
+ def with_project_root(dir)
26
+ RuboCop::ConfigFinder.project_root = dir
27
+ yield
28
+ ensure
29
+ RuboCop::ConfigFinder.project_root = nil
30
+ end
31
+
32
+ def target_ruby(dir)
33
+ RuboCop::TargetRuby.new(RuboCop::ConfigLoader.configuration_from_file(File.join(dir, '.rubocop.yml'))).version
34
+ end
35
+
36
+ def lint(dir)
37
+ report, exit_code = capture_rubocop_json(['--config', File.join(dir, '.rubocop.yml'), '--cache', 'false',
38
+ '--format', 'json', dir])
39
+ offenses = report.fetch('files').flat_map do |file|
40
+ path = file.fetch('path').delete_prefix("#{dir}/")
41
+ file.fetch('offenses').map do |offense|
42
+ "#{path}:#{offense.dig('location', 'line')}: #{offense.fetch('cop_name')}: #{offense.fetch('message')}"
43
+ end
44
+ end
45
+ { offenses: offenses, inspected: report.fetch('summary').fetch('inspected_file_count'), exit_code: exit_code }
46
+ end
47
+
48
+ def capture_rubocop_json(args)
49
+ original = $stdout
50
+ $stdout = StringIO.new
51
+ exit_code = RuboCop::CLI.new.run(args)
52
+ [JSON.parse($stdout.string), exit_code]
53
+ ensure
54
+ $stdout = original
55
+ end
56
+
57
+ def expect_no_offenses(project_name)
58
+ expect(File).to exist(File.join(project_name, '.rubocop.yml'))
59
+ rubocop_report(project_name) do |summary, target|
60
+ offenses = summary.fetch(:offenses)
61
+ expect(target).to eq(RuboCop::TargetRuby::DEFAULT_VERSION)
62
+ expect(summary.fetch(:inspected)).to be_positive, "#{project_name}: RuboCop inspected no files"
63
+ expect(offenses).to be_empty, "#{project_name} has #{offenses.size} RuboCop offenses:\n#{offenses.join("\n")}"
64
+ expect(summary.fetch(:exit_code)).to eq(0)
65
+ end
66
+ end
67
+
68
+ FRAMEWORKS.each do |framework|
69
+ AUTOMATION_TYPES.each do |automation|
70
+ { 'with examples' => true, 'without examples' => false }.each do |label, examples|
71
+ it "has no offenses for #{framework} and #{automation} (#{label})" do
72
+ expect_no_offenses(ensure_project(framework:, automation:, examples:))
73
+ end
74
+ end
75
+ end
76
+
77
+ RUBOCOP_WEB_AUTOMATIONS.each do |automation|
78
+ it "has no offenses for #{framework} and #{automation} with accessibility" do
79
+ name = "rubocop_axe_#{framework}_#{automation}"
80
+ InvokeGenerators.generate_framework(automation:, framework:, name:, accessibility: true)
81
+ expect_no_offenses(name)
82
+ ensure
83
+ FileUtils.rm_rf(name)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -2,66 +2,46 @@
2
2
 
3
3
  require_relative 'content_helper'
4
4
 
5
- SYNTAX_WEB_AUTOMATIONS = %w[selenium watir].freeze
6
- SYNTAX_ALL_FRAMEWORKS = %w[cucumber rspec].freeze
5
+ SYNTAX_MOBILE_AUTOMATIONS = %w[android ios cross_platform].freeze
7
6
 
7
+ # Projects are generated by spec_helper's before(:all), so the file list is read inside the
8
+ # examples (after generation), never when this file loads.
8
9
  describe 'Ruby syntax validation' do
9
- SYNTAX_ALL_FRAMEWORKS.each do |framework|
10
- SYNTAX_WEB_AUTOMATIONS.each do |automation|
11
- context "with #{framework} and #{automation}" do
12
- project_name = "#{framework}_#{automation}"
13
-
14
- Dir.glob("#{project_name}/**/*.rb").each do |file|
15
- relative = file.sub("#{project_name}/", '')
16
-
17
- it "#{relative} has valid Ruby syntax" do
18
- content = File.read(file)
19
- expect(content).to have_valid_ruby_syntax
20
- end
21
-
22
- it "#{relative} has frozen_string_literal" do
23
- content = File.read(file)
24
- expect(content).to have_frozen_string_literal
25
- end
26
- end
10
+ def generated_ruby_files(project_name)
11
+ Dir.glob('**/*.rb', base: project_name).sort
12
+ end
27
13
 
28
- login_reuse_files = %w[helpers/session_state.rb]
29
- login_reuse_files += if framework == 'rspec'
30
- %w[helpers/spec_helper.rb]
31
- else
32
- %w[features/support/env.rb features/step_definitions/login_steps.rb]
33
- end
34
- # The glob above runs at load time, before the projects exist, so new helpers are listed here.
35
- login_reuse_files += %w[helpers/wait_helper.rb helpers/raider_element.rb] if automation == 'selenium'
14
+ def syntax_error_for(content)
15
+ RubyVM::InstructionSequence.compile(content)
16
+ nil
17
+ rescue SyntaxError => e
18
+ e.message
19
+ end
36
20
 
37
- login_reuse_files.each do |relative|
38
- it "#{relative} is generated as valid Ruby with frozen_string_literal" do
39
- content = File.read(File.join(project_name, relative))
40
- expect(content).to have_valid_ruby_syntax
41
- expect(content).to have_frozen_string_literal
21
+ FRAMEWORKS.each do |framework|
22
+ AUTOMATION_TYPES.each do |automation|
23
+ { 'with examples' => true, 'without examples' => false }.each do |label, examples|
24
+ it "generates only valid Ruby with frozen_string_literal for #{framework} and #{automation} (#{label})" do
25
+ project_name = ensure_project(framework:, automation:, examples:)
26
+ files = generated_ruby_files(project_name)
27
+
28
+ expect(files).not_to be_empty
29
+ aggregate_failures do
30
+ files.each do |relative|
31
+ content = File.read(File.join(project_name, relative))
32
+ expect(syntax_error_for(content)).to be_nil, "#{project_name}/#{relative} has a syntax error"
33
+ expect(content).to have_frozen_string_literal, "#{project_name}/#{relative} lacks frozen_string_literal"
34
+ end
42
35
  end
43
36
  end
44
37
  end
45
38
  end
46
39
 
47
- # Mobile projects get the mobile branch of the wait helper templates.
48
- %w[android ios cross_platform].each do |automation|
49
- context "with #{framework} and #{automation}" do
50
- project_name = "#{framework}_#{automation}"
51
-
52
- %w[helpers/wait_helper.rb helpers/raider_element.rb].each do |relative|
53
- it "#{relative} is generated as valid Ruby with frozen_string_literal" do
54
- content = File.read(File.join(project_name, relative))
55
- expect(content).to have_valid_ruby_syntax
56
- expect(content).to have_frozen_string_literal
57
- end
58
- end
59
-
60
- it 'helpers/wait_helper.rb has the mobile gestures and no web-only methods' do
61
- content = File.read(File.join(project_name, 'helpers/wait_helper.rb'))
62
- expect(content).to include("'mobile: swipeGesture'").and include('def scroll_to(')
63
- expect(content).not_to include('def drag_and_drop(')
64
- end
40
+ SYNTAX_MOBILE_AUTOMATIONS.each do |automation|
41
+ it "#{framework}_#{automation} wait_helper has the mobile gestures and no web-only methods" do
42
+ content = File.read(File.join(ensure_project(framework:, automation:), 'helpers/wait_helper.rb'))
43
+ expect(content).to include("'mobile: swipeGesture'").and include('def scroll_to(')
44
+ expect(content).not_to include('def drag_and_drop(')
65
45
  end
66
46
  end
67
47
  end
@@ -212,10 +212,13 @@ describe 'Example tests on the wait helper' do
212
212
  end
213
213
  end
214
214
 
215
- # The Watir output must stay as master generated it (1274a66); the snapshots in
215
+ # The Watir output must stay as master generated it (1274a66), plus the 3.12.1 lint fixes; the snapshots in
216
216
  # spec/fixtures/watir_master were generated from that commit's lib.
217
217
  # To regenerate: `git archive 1274a66 lib` into a scratch dir, generate rspec and cucumber Watir projects there,
218
- # then apply only the Readme deprecation-line delta (the `> **Deprecated:**` line under the heading block).
218
+ # then apply only the Readme deprecation-line delta (the `> **Deprecated:**` line under the heading block) and
219
+ # the 3.12.1 changes: RuboCop lint fixes (page.rb loses its extra blank lines, login_steps.rb ends with a newline)
220
+ # plus the frozen_string_literal comments the syntax spec requires (Style/FrozenStringLiteralComment is disabled
221
+ # in the generated .rubocop.yml, so RuboCop does not ask for them) on the Watir page, world and user_factory files.
219
222
  describe 'Watir example output' do
220
223
  snapshots = File.expand_path('../../fixtures/watir_master', __dir__)
221
224