ruby_raider 3.12.0 → 3.12.1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/lib/generators/automation/templates/page.tt +8 -3
  4. data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -3
  5. data/lib/generators/automation/templates/partials/url_methods.tt +3 -2
  6. data/lib/generators/automation/templates/partials/visit_method.tt +5 -5
  7. data/lib/generators/cucumber/cucumber_generator.rb +1 -1
  8. data/lib/generators/cucumber/templates/env.tt +3 -3
  9. data/lib/generators/cucumber/templates/steps.tt +2 -2
  10. data/lib/generators/cucumber/templates/user_factory.tt +2 -0
  11. data/lib/generators/cucumber/templates/world.tt +5 -3
  12. data/lib/generators/generator.rb +15 -11
  13. data/lib/generators/rspec/templates/accessibility_spec.tt +10 -10
  14. data/lib/generators/rspec/templates/spec.tt +2 -1
  15. data/lib/generators/templates/common/gemfile.tt +9 -9
  16. data/lib/generators/templates/helpers/appium_helper.tt +17 -15
  17. data/lib/generators/templates/helpers/browser_helper.tt +1 -2
  18. data/lib/generators/templates/helpers/partials/appium_driver.tt +1 -1
  19. data/lib/generators/templates/helpers/session_state.tt +1 -1
  20. data/lib/generators/templates/helpers/spec_helper.tt +1 -1
  21. data/lib/ruby_raider.rb +9 -2
  22. data/lib/version +1 -1
  23. data/sig/generators/generator.rbs +2 -0
  24. data/sig/ruby_raider.rbs +1 -0
  25. data/spec/commands/raider_commands_spec.rb +25 -1
  26. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +1 -1
  27. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +2 -4
  28. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +2 -4
  29. data/spec/integration/content/content_helper.rb +1 -5
  30. data/spec/integration/content/rubocop_content_spec.rb +87 -0
  31. data/spec/integration/content/syntax_validation_spec.rb +31 -51
  32. data/spec/integration/content/test_content_spec.rb +5 -2
  33. data/spec/integration/end_to_end_spec.rb +23 -0
  34. data/spec/integration/frozen_string_literal_matcher.rb +13 -0
  35. data/spec/integration/generators/invoke_generators_spec.rb +60 -0
  36. data/spec/integration/scaffolding_e2e_spec.rb +1 -5
  37. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3dd26171963a22a6a95ac06de57c7baf3e462a8d0d71049b88b99d40552c94c0
4
- data.tar.gz: 9826176205e621abddb2ddd583aa3bdee28eacd3c5e7d00a606bda9b2ba443e1
3
+ metadata.gz: e1312bb43aceb9be9a70d5e3d09e6694d901f6b5dc3ec80f27b6973de6c8d60c
4
+ data.tar.gz: ad4e86e9f7dd6bbbacac033fe48f18a93d6397040ab29490d0add8456f6ad2cc
5
5
  SHA512:
6
- metadata.gz: 3f0b129f291c538614a488c47f3ce71971f24060b60719203df43a2a19cc82309141cc846c867b644bb54832e9bbee2390fee04c2bb1db7bcec0c1f200d3eecb
7
- data.tar.gz: 2d5e307edb7b5fdc1fbb0f1c0a1c830c6489fd8c7a11b3c08827fb3df9c49e7f1e23493a9165f23a5b2cbd7cd465913486fe174c79b7668731b7e5c7359e3e49
6
+ metadata.gz: b913021e191c5ff9fddc114590bc7b6a4a1b0e340a0fb2257da5477c2e3344dcd73088ea1cbac4eb1b28953b7bc3c250d56c648f15e85977088b8d96d3ddbc2b
7
+ data.tar.gz: c1d12da3f4447bbabecc6638895129dc0dd9d23dc08389a8b9d6ee9077c3936855b28746b9a6d9d049b3ced2c2e296462154c88e1845d6357078bb41ab8f75f6
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.1
1
+ 3.4.12
@@ -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 -%>
@@ -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?
@@ -76,7 +75,7 @@ class Generator < Thor::Group
76
75
 
77
76
  # False for `raider new -p examples:false`: an empty skeleton without the example pages, tests and wiring
78
77
  def examples?
79
- !args.drop(3).include?('no_examples')
78
+ !options_after_name.include?('no_examples')
80
79
  end
81
80
 
82
81
  def web?
@@ -84,7 +83,7 @@ class Generator < Thor::Group
84
83
  end
85
84
 
86
85
  def axe_addon?
87
- args.include?('axe_addon')
86
+ options_after_name.include?('axe_addon')
88
87
  end
89
88
 
90
89
  def selenium_based?
@@ -98,6 +97,11 @@ class Generator < Thor::Group
98
97
 
99
98
  private
100
99
 
100
+ # Option flags only: everything after automation, framework and name
101
+ def options_after_name
102
+ args.drop(3)
103
+ end
104
+
101
105
  def _initializer
102
106
  @_initializer ||= super
103
107
  end
@@ -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.10'
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? -%>
@@ -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
@@ -32,7 +32,7 @@
32
32
  end
33
33
  <%- end -%>
34
34
 
35
- <%= partial('browserstack_config', strip: true) %>
35
+ <%= partial('browserstack_config', strip: true) %>
36
36
 
37
37
  def configure_driver
38
38
  if browserstack?
@@ -17,7 +17,7 @@ require 'yaml'
17
17
  class SessionState
18
18
  FORMAT = 1
19
19
  DEFAULT_MAX_AGE = 3600
20
- NAME_PATTERN = /\A[a-z0-9_-]+\z/.freeze
20
+ NAME_PATTERN = Regexp.new('\A[a-z0-9_-]+\z').freeze
21
21
  DIR = File.join('.raider', 'auth')
22
22
  READ_STORAGE = 'return Object.assign({}, window[arguments[0]]);'
23
23
  WRITE_STORAGE = 'var s = window[arguments[0]]; s.clear(); var d = arguments[1]; ' \
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- <%- if axe_addon? %>
3
+ <%- if axe_addon? -%>
4
4
  require 'axe-rspec'
5
5
  <%- end -%>
6
6
  require 'rspec'
data/lib/ruby_raider.rb CHANGED
@@ -52,12 +52,19 @@ module RubyRaider
52
52
  map 'u' => 'utility'
53
53
 
54
54
  no_tasks do
55
- # -p values arrive as strings; only examples:false turns the example tests off
55
+ # -p values arrive as strings; examples must be true or false, in any case
56
56
  def generate_from_parameters(params)
57
- params[:examples] = params[:examples].to_s.downcase != 'false' if params.key?(:examples)
57
+ params[:examples] = parse_examples(params[:examples]) if params.key?(:examples)
58
58
  InvokeGenerators.generate_framework(params)
59
59
  end
60
60
 
61
+ def parse_examples(value)
62
+ normalised = value.to_s.downcase
63
+ return normalised == 'true' if %w[true false].include?(normalised)
64
+
65
+ raise Thor::Error, "Invalid examples #{value.to_s.inspect}. Allowed values: true, false"
66
+ end
67
+
61
68
  def merge_flags(params)
62
69
  params[:accessibility] = true if options[:accessibility]
63
70
  end
data/lib/version CHANGED
@@ -1 +1 @@
1
- 3.12.0
1
+ 3.12.1
@@ -29,5 +29,7 @@ class Generator < Thor::Group
29
29
 
30
30
  private
31
31
 
32
+ def options_after_name: () -> Array[untyped]
33
+
32
34
  def _initializer: () -> Array[untyped]
33
35
  end
data/sig/ruby_raider.rbs CHANGED
@@ -8,6 +8,7 @@ module RubyRaider
8
8
 
9
9
  private
10
10
 
11
+ def parse_examples: (untyped value) -> bool
11
12
  def generate_from_parameters: (Hash[Symbol, untyped] params) -> void
12
13
  def merge_flags: (Hash[Symbol, untyped] params) -> void
13
14
  end
@@ -33,7 +33,31 @@ RSpec.describe RubyRaider::Raider do # rubocop:disable RSpec/FilePath
33
33
  expect(InvokeGenerators).to have_received(:generate_framework).with(hash_excluding(:examples))
34
34
  end
35
35
 
36
- it 'keeps the examples for any value other than false' do
36
+ it 'accepts examples:FALSE in any case as false' do
37
+ allow(InvokeGenerators).to receive(:generate_framework)
38
+
39
+ described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:FALSE])
40
+
41
+ expect(InvokeGenerators).to have_received(:generate_framework).with(hash_including(examples: false))
42
+ end
43
+
44
+ it 'accepts examples:True in any case as true' do
45
+ allow(InvokeGenerators).to receive(:generate_framework)
46
+
47
+ described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:True])
48
+
49
+ expect(InvokeGenerators).to have_received(:generate_framework).with(hash_including(examples: true))
50
+ end
51
+
52
+ it 'rejects any other examples value before generating' do
53
+ allow(InvokeGenerators).to receive(:generate_framework)
54
+
55
+ expect { described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:no]) }
56
+ .to raise_error(Thor::Error, 'Invalid examples "no". Allowed values: true, false')
57
+ expect(InvokeGenerators).not_to have_received(:generate_framework)
58
+ end
59
+
60
+ it 'keeps the examples for true' do
37
61
  allow(InvokeGenerators).to receive(:generate_framework)
38
62
 
39
63
  described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:true])
@@ -21,4 +21,4 @@ Then("I'm logged in") do
21
21
  account_page = Account.new(browser)
22
22
  account_page.visit
23
23
  expect(account_page.header.customer_name).to eq "Welcome back #{@user['name']}"
24
- end
24
+ end
@@ -1,20 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../components/header'
2
4
 
3
5
  class Page
4
-
5
6
  attr_reader :browser
6
7
 
7
8
  def initialize(browser)
8
9
  @browser = browser
9
10
  end
10
11
 
11
-
12
12
  def visit(*page)
13
13
  @browser.goto full_url(page.first)
14
14
  end
15
15
 
16
-
17
-
18
16
  def full_url(*page)
19
17
  "#{base_url}#{url(*page)}"
20
18
  end
@@ -1,20 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../components/header'
2
4
 
3
5
  class Page
4
-
5
6
  attr_reader :browser
6
7
 
7
8
  def initialize(browser)
8
9
  @browser = browser
9
10
  end
10
11
 
11
-
12
12
  def visit(*page)
13
13
  @browser.goto full_url(page.first)
14
14
  end
15
15
 
16
-
17
-
18
16
  def full_url(*page)
19
17
  "#{base_url}#{url(*page)}"
20
18
  end
@@ -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)
@@ -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
 
@@ -265,11 +265,30 @@ describe 'End-to-End Project Generation and Execution' do
265
265
  FileUtils.rm_f(File.join(project, 'features/step_definitions/reporter_failure_steps.rb'))
266
266
  end
267
267
 
268
+ # Runs the project's own RuboCop with the gems its bundle resolved (newer than this gem's dev bundle).
269
+ # It runs from a copy outside this repo, as a user's project would: inside it RuboCop would also read
270
+ # this repo's .rubocop.yml, gemspec and .ruby-version.
271
+ def expect_project_rubocop_clean(project)
272
+ skip 'Bundle install failed' unless bundle_install(project)
273
+
274
+ Dir.mktmpdir('raider_rubocop') do |tmp|
275
+ copy = File.join(tmp, project)
276
+ FileUtils.cp_r(project, copy)
277
+ puts run_in_project(copy, 'bundle exec rubocop -V', timeout: 60)[:stdout]
278
+ result = run_in_project(copy, 'bundle exec rubocop --cache false', timeout: 180)
279
+ expect(result[:exit_code]).to eq(0), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
280
+ end
281
+ end
282
+
268
283
  # Test Web Frameworks (these can run without external services)
269
284
  context 'Web Automation Frameworks' do
270
285
  describe 'Selenium + RSpec' do
271
286
  include_examples 'executable rspec project', 'rspec_selenium'
272
287
 
288
+ it 'passes its own RuboCop with its bundled gems', :slow do
289
+ expect_project_rubocop_clean('rspec_selenium')
290
+ end
291
+
273
292
  it 'saves the login once and reuses it', :aggregate_failures, :slow do
274
293
  skip 'Bundle install failed' unless bundle_install('rspec_selenium')
275
294
 
@@ -300,6 +319,10 @@ describe 'End-to-End Project Generation and Execution' do
300
319
 
301
320
  describe 'Selenium + Cucumber' do
302
321
  include_examples 'executable cucumber project', 'cucumber_selenium'
322
+
323
+ it 'passes its own RuboCop with its bundled gems', :slow do
324
+ expect_project_rubocop_clean('cucumber_selenium')
325
+ end
303
326
  end
304
327
 
305
328
  describe 'Watir + Cucumber' do
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+
5
+ # Ruby only reads the magic comment in the comment block before the first line of code. Ruby would
6
+ # accept it indented, but the generated project's RuboCop (Layout/CommentIndentation) would not.
7
+ RSpec::Matchers.define :have_frozen_string_literal do
8
+ match do |content|
9
+ leading_comments = content.lines.take_while { |line| line.strip.empty? || line.start_with?('#') }
10
+ leading_comments.any? { |line| line.chomp == '# frozen_string_literal: true' }
11
+ end
12
+ failure_message { 'expected an unindented frozen_string_literal magic comment before the first line of code' }
13
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'tmpdir'
4
5
  require 'open3'
5
6
  require_relative '../spec_helper'
6
7
 
@@ -120,3 +121,62 @@ describe 'Projects named like a generator option' do
120
121
  expect(File.read('selenium/page_objects/abstract/page.rb')).not_to include('find_element')
121
122
  end
122
123
  end
124
+
125
+ describe 'Option-named projects generate what a neutral name generates' do
126
+ option_names = %w[ios android cucumber rspec cross_platform axe_addon selenium watir no_examples azure]
127
+ option_sets = [
128
+ { automation: 'selenium', framework: 'rspec' },
129
+ { automation: 'selenium', framework: 'cucumber', accessibility: true },
130
+ { automation: 'selenium', framework: 'rspec', examples: false, ci: 'azure' },
131
+ { automation: 'watir', framework: 'cucumber' },
132
+ { automation: 'ios', framework: 'rspec' },
133
+ { automation: 'android', framework: 'cucumber' },
134
+ { automation: 'cross_platform', framework: 'rspec' }
135
+ ].freeze
136
+
137
+ def quietly
138
+ result = nil
139
+ expect { result = yield }.to output.to_stdout
140
+ result
141
+ end
142
+
143
+ def snapshot(name, options)
144
+ Dir.mktmpdir do |dir|
145
+ Dir.chdir(dir) do
146
+ InvokeGenerators.generate_framework(options.merge(name:))
147
+ Dir.glob("#{name}/**/*", File::FNM_DOTMATCH).select { |f| File.file?(f) }
148
+ .to_h { |f| [f.delete_prefix("#{name}/"), File.read(f)] }
149
+ end
150
+ end
151
+ end
152
+
153
+ option_sets.each do |options|
154
+ context "with #{options}" do
155
+ let(:neutral) { quietly { snapshot('demo', options) } }
156
+
157
+ option_names.each do |name|
158
+ it "generates the same files for a project named #{name}" do
159
+ actual = quietly { snapshot(name, options) }
160
+ expect(actual.keys.sort).to eq(neutral.keys.sort)
161
+ expect(actual).to eq(neutral)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ describe 'raider new -p examples: validation' do
169
+ after { FileUtils.rm_rf(%w[bad_examples]) }
170
+
171
+ let(:root) { File.expand_path('../../..', __dir__) }
172
+
173
+ it 'rejects an unknown value, names the allowed values and generates nothing' do
174
+ Dir.mktmpdir do |dir|
175
+ _out, err, status = Open3.capture3('ruby', "#{root}/bin/raider", 'new', 'x', '-p',
176
+ 'framework:rspec', 'automation:selenium', 'examples:no', chdir: dir)
177
+ expect(status.exitstatus).to eq(1)
178
+ expect(err).to include('Invalid examples "no". Allowed values: true, false')
179
+ expect(File).not_to exist(File.join(dir, 'x'))
180
+ end
181
+ end
182
+ end
@@ -4,13 +4,9 @@ require 'fileutils'
4
4
  require 'pathname'
5
5
  require_relative '../../lib/commands/scaffolding_commands'
6
6
  require_relative '../../lib/scaffolding/scaffolding'
7
+ require_relative 'frozen_string_literal_matcher'
7
8
 
8
9
  # Custom matchers (defined inline to avoid pulling in integration spec_helper)
9
- RSpec::Matchers.define :have_frozen_string_literal do
10
- match { |content| content.include?('# frozen_string_literal: true') }
11
- failure_message { 'expected file to contain frozen_string_literal magic comment' }
12
- end
13
-
14
10
  RSpec::Matchers.define :have_valid_ruby_syntax do
15
11
  match do |content|
16
12
  RubyVM::InstructionSequence.compile(content)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_raider
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.12.0
4
+ version: 3.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Agustin Pequeno
@@ -368,10 +368,12 @@ files:
368
368
  - spec/integration/content/page_content_spec.rb
369
369
  - spec/integration/content/readme_content_spec.rb
370
370
  - spec/integration/content/rspec_reporter_content_spec.rb
371
+ - spec/integration/content/rubocop_content_spec.rb
371
372
  - spec/integration/content/syntax_validation_spec.rb
372
373
  - spec/integration/content/test_content_spec.rb
373
374
  - spec/integration/end_to_end_features_spec.rb
374
375
  - spec/integration/end_to_end_spec.rb
376
+ - spec/integration/frozen_string_literal_matcher.rb
375
377
  - spec/integration/generators/automation_generator_spec.rb
376
378
  - spec/integration/generators/axe_addon_spec.rb
377
379
  - spec/integration/generators/azure_generator_spec.rb