ruby_raider 3.11.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/reek.yml +5 -1
  3. data/.github/workflows/release.yml +2 -1
  4. data/.github/workflows/system_tests.yml +1 -1
  5. data/.reek.yml +13 -0
  6. data/.ruby-version +1 -1
  7. data/README.md +43 -4
  8. data/lib/commands/utility_commands.rb +1 -1
  9. data/lib/generators/automation/automation_generator.rb +15 -10
  10. data/lib/generators/automation/templates/appium_caps.tt +2 -4
  11. data/lib/generators/automation/templates/home.tt +1 -2
  12. data/lib/generators/automation/templates/login.tt +6 -6
  13. data/lib/generators/automation/templates/page.tt +18 -6
  14. data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
  15. data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -3
  16. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
  17. data/lib/generators/automation/templates/partials/url_methods.tt +3 -2
  18. data/lib/generators/automation/templates/partials/visit_method.tt +5 -5
  19. data/lib/generators/automation/templates/pdp.tt +3 -2
  20. data/lib/generators/cucumber/cucumber_generator.rb +16 -3
  21. data/lib/generators/cucumber/templates/env.tt +3 -3
  22. data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
  23. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
  24. data/lib/generators/cucumber/templates/partials/selenium_env.tt +4 -0
  25. data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
  26. data/lib/generators/cucumber/templates/steps.tt +2 -2
  27. data/lib/generators/cucumber/templates/user_factory.tt +2 -0
  28. data/lib/generators/cucumber/templates/world.tt +5 -3
  29. data/lib/generators/generator.rb +29 -14
  30. data/lib/generators/helper_generator.rb +8 -0
  31. data/lib/generators/invoke_generators.rb +23 -0
  32. data/lib/generators/menu_generator.rb +26 -14
  33. data/lib/generators/rspec/rspec_generator.rb +10 -3
  34. data/lib/generators/rspec/templates/accessibility_spec.tt +10 -10
  35. data/lib/generators/rspec/templates/spec.tt +17 -2
  36. data/lib/generators/template_renderer/template_error.rb +8 -8
  37. data/lib/generators/templates/common/gemfile.tt +9 -9
  38. data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
  39. data/lib/generators/templates/common/partials/web_config.tt +1 -1
  40. data/lib/generators/templates/common/read_me.tt +508 -24
  41. data/lib/generators/templates/helpers/appium_helper.tt +17 -15
  42. data/lib/generators/templates/helpers/browser_helper.tt +1 -2
  43. data/lib/generators/templates/helpers/partials/appium_driver.tt +1 -1
  44. data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
  45. data/lib/generators/templates/helpers/raider_element.tt +159 -0
  46. data/lib/generators/templates/helpers/session_state.tt +46 -6
  47. data/lib/generators/templates/helpers/spec_helper.tt +10 -2
  48. data/lib/generators/templates/helpers/wait_helper.tt +470 -0
  49. data/lib/ruby_raider/ffmpeg.rb +7 -7
  50. data/lib/ruby_raider.rb +9 -0
  51. data/lib/scaffolding/project_detector.rb +6 -0
  52. data/lib/scaffolding/scaffolding.rb +4 -0
  53. data/lib/scaffolding/templates/component.tt +4 -0
  54. data/lib/scaffolding/templates/page_object.tt +8 -0
  55. data/lib/version +1 -1
  56. data/sig/generators/generator.rbs +4 -0
  57. data/sig/ruby_raider.rbs +1 -0
  58. data/sig/scaffolding/project_detector.rbs +1 -0
  59. data/sig/scaffolding/scaffolding.rbs +1 -0
  60. data/spec/commands/raider_commands_spec.rb +48 -0
  61. data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
  62. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
  63. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +37 -0
  64. data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
  65. data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
  66. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +37 -0
  67. data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
  68. data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
  69. data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
  70. data/spec/integration/commands/utility_commands_spec.rb +8 -0
  71. data/spec/integration/content/config_content_spec.rb +44 -0
  72. data/spec/integration/content/content_helper.rb +1 -5
  73. data/spec/integration/content/helper_content_spec.rb +79 -0
  74. data/spec/integration/content/page_content_spec.rb +75 -4
  75. data/spec/integration/content/readme_content_spec.rb +126 -5
  76. data/spec/integration/content/rubocop_content_spec.rb +87 -0
  77. data/spec/integration/content/syntax_validation_spec.rb +33 -30
  78. data/spec/integration/content/test_content_spec.rb +60 -0
  79. data/spec/integration/end_to_end_spec.rb +73 -28
  80. data/spec/integration/frozen_string_literal_matcher.rb +13 -0
  81. data/spec/integration/generators/automation_generator_spec.rb +50 -0
  82. data/spec/integration/generators/common_generator_spec.rb +20 -0
  83. data/spec/integration/generators/config_features_spec.rb +2 -2
  84. data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
  85. data/spec/integration/generators/helpers_generator_spec.rb +38 -0
  86. data/spec/integration/generators/invoke_generators_spec.rb +130 -0
  87. data/spec/integration/generators/rspec_generator_spec.rb +20 -0
  88. data/spec/integration/helpers/session_state_spec.rb +46 -0
  89. data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
  90. data/spec/integration/helpers/wait_helper_spec.rb +496 -0
  91. data/spec/integration/helpers/wait_helper_support.rb +183 -0
  92. data/spec/integration/scaffolding_e2e_spec.rb +1 -5
  93. data/spec/integration/settings_helper.rb +4 -1
  94. data/spec/integration/spec_helper.rb +2 -2
  95. data/spec/menus/menu_generator_spec.rb +64 -2
  96. data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
  97. metadata +17 -2
@@ -110,4 +110,12 @@ describe UtilityCommands do
110
110
  expect(config.dig('browser_arguments', 'firefox')).to eq([])
111
111
  end
112
112
  end
113
+
114
+ # Generated drivers connect to appium_lib's default, http://127.0.0.1:4723, with no base path.
115
+ it 'starts Appium on the default path the generated drivers connect to' do
116
+ command = utility.new
117
+ allow(command).to receive(:system)
118
+ command.start_appium
119
+ expect(command).to have_received(:system).with('appium')
120
+ end
113
121
  end
@@ -111,6 +111,23 @@ describe 'Config file content' do
111
111
  end
112
112
  end
113
113
 
114
+ # appium_lib connects to http://127.0.0.1:4723 and ignores a url capability, so none is generated.
115
+ %w[rspec cucumber].product(%w[android ios cross_platform]).each do |framework, automation|
116
+ it "has no unused url capability or /wd/hub path in #{framework} #{automation} capabilities" do
117
+ caps = read_generated("#{framework}_#{automation}", 'config/capabilities.yml')
118
+ local = caps.split(/^browserstack:/).first
119
+ expect(local).not_to match(/^\s*url:/)
120
+ expect(local).not_to include('/wd/hub')
121
+ end
122
+
123
+ it "parses as YAML, BrowserStack block included, in #{framework} #{automation} capabilities" do
124
+ caps = YAML.safe_load(read_generated("#{framework}_#{automation}", 'config/capabilities.yml'))
125
+ browserstack = caps.fetch('browserstack')
126
+ expect(browserstack).to have_key('app')
127
+ expect(browserstack['app'].to_s).not_to start_with('app:')
128
+ end
129
+ end
130
+
114
131
  %w[android ios cross_platform].each do |automation|
115
132
  it "has no login_state in the #{automation} config" do
116
133
  Dir.glob(File.join("#{FrameworkIndex::RSPEC}_#{automation}", 'config', '*.yml')).each do |file|
@@ -119,3 +136,30 @@ describe 'Config file content' do
119
136
  end
120
137
  end
121
138
  end
139
+
140
+ describe 'Config timeout wiring' do
141
+ %w[rspec cucumber].each do |framework|
142
+ # Single-platform Appium projects have no config.yml; WaitHelper falls back to its default of 10.
143
+ %w[selenium cross_platform].each do |automation|
144
+ it "sets timeout 10 in #{framework} #{automation}" do
145
+ config = YAML.safe_load(read_generated("#{framework}_#{automation}", 'config/config.yml'), permitted_classes: [Symbol])
146
+ expect(config['timeout']).to eq(10)
147
+ end
148
+ end
149
+ end
150
+
151
+ it 'describes the selenium timeout as an action and eventually wait' do
152
+ expect(read_generated('rspec_selenium', 'config/config.yml'))
153
+ .to include('# seconds that actions and eventually wait before failing')
154
+ end
155
+
156
+ it 'describes the mobile timeout as an action and eventually wait' do
157
+ expect(read_generated('rspec_cross_platform', 'config/config.yml'))
158
+ .to include('timeout: 10 # seconds that actions and eventually wait before failing')
159
+ end
160
+
161
+ it 'leaves the watir timeout comment unchanged' do
162
+ expect(read_generated('rspec_watir', 'config/config.yml'))
163
+ .to include('timeout: 10 # seconds — used for implicit waits and max wait time')
164
+ end
165
+ 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)
@@ -398,3 +398,82 @@ describe 'Helper file content' do
398
398
  end
399
399
  end
400
400
  end
401
+
402
+ describe 'Auto-waiting helper wiring' do
403
+ %w[selenium ios android cross_platform].each do |automation|
404
+ context "with rspec and #{automation}" do
405
+ it 'includes WaitHelper in the spec helper' do
406
+ helper = read_generated("#{FrameworkIndex::RSPEC}_#{automation}", 'helpers/spec_helper.rb')
407
+ expect(helper).to include("require_relative 'wait_helper'", 'config.include(WaitHelper)')
408
+ end
409
+ end
410
+ end
411
+
412
+ %w[ios android cross_platform].each do |automation|
413
+ %w[rspec cucumber].each do |framework|
414
+ it "does not pass a wait to appium_lib in #{framework} #{automation}" do
415
+ driver = read_generated("#{framework}_#{automation}", 'helpers/driver_helper.rb')
416
+ expect(driver).not_to match(/\bwait:/)
417
+ end
418
+ end
419
+ end
420
+
421
+ %w[rspec cucumber].each do |framework|
422
+ it "returns the new driver without a redundant assignment in #{framework} selenium" do
423
+ driver = read_generated("#{framework}_selenium", 'helpers/driver_helper.rb')
424
+ expect(driver).to include(" Selenium::WebDriver.for(browser.to_sym, **arguments)\n end")
425
+ expect(driver).not_to include('driver = Selenium::WebDriver.for')
426
+ end
427
+
428
+ it "leaves no implicit wait in #{framework} selenium" do
429
+ driver = read_generated("#{framework}_selenium", 'helpers/driver_helper.rb')
430
+ expect(driver).not_to include('implicit_wait')
431
+ end
432
+
433
+ it "keeps Watir untouched in #{framework} watir" do
434
+ project = "#{framework}_watir"
435
+ expect(File.exist?(File.join(project, 'helpers/wait_helper.rb'))).to be(false)
436
+ expect(read_generated(project, 'helpers/browser_helper.rb')).to include('Watir.default_timeout')
437
+ files = Dir.glob(File.join(project, '**/*.rb')).map { |f| File.read(f) }
438
+ expect(files.grep(/WaitHelper/)).to be_empty
439
+ end
440
+ end
441
+
442
+ describe 'without example tests' do
443
+ %w[selenium ios cross_platform].each do |automation|
444
+ context "with rspec and #{automation}" do
445
+ subject(:helper) { read_generated(ensure_project(framework: 'rspec', automation:, examples: false), 'helpers/spec_helper.rb') }
446
+
447
+ it 'names no example class or hook' do
448
+ expect(helper).not_to match(/Login|Account|logged_in|model_factory/)
449
+ end
450
+
451
+ it 'has valid Ruby syntax' do
452
+ expect(helper).to have_valid_ruby_syntax
453
+ end
454
+ end
455
+
456
+ context "with cucumber and #{automation}" do
457
+ let(:name) { ensure_project(framework: 'cucumber', automation:, examples: false) }
458
+
459
+ it 'names no example class or hook in the env and the World' do
460
+ %w[env.rb world.rb].each do |file|
461
+ expect(read_generated(name, "features/support/#{file}")).not_to match(/Login|Account|logged_in|factory/)
462
+ end
463
+ end
464
+
465
+ it 'has a valid env and World' do
466
+ %w[env.rb world.rb].each do |file|
467
+ expect(read_generated(name, "features/support/#{file}")).to have_valid_ruby_syntax
468
+ end
469
+ end
470
+ end
471
+ end
472
+
473
+ it 'keeps the session state require in the selenium rspec spec helper' do
474
+ helper = read_generated(ensure_project(framework: 'rspec', automation: 'selenium', examples: false),
475
+ 'helpers/spec_helper.rb')
476
+ expect(helper).to include("require_relative 'session_state'")
477
+ end
478
+ end
479
+ end
@@ -36,12 +36,16 @@ describe 'Page object content' do
36
36
  shared_examples 'selenium login page' do |project_name|
37
37
  subject(:login) { read_generated(project_name, 'page_objects/pages/login.rb') }
38
38
 
39
- it 'uses find_element for elements' do
40
- expect(login).to include('driver.find_element')
39
+ it 'returns locator hashes from the element methods' do
40
+ expect(login).to include("{ id: 'loginFrm_loginname' }", "{ id: 'loginFrm_password' }",
41
+ %q({ xpath: "//button[@title='Login']" }))
42
+ expect(login).not_to include('driver.find_element')
41
43
  end
42
44
 
43
- it 'uses send_keys for input' do
44
- expect(login).to include('send_keys')
45
+ it 'logs in through the wait helper' do
46
+ expect(login).to include('send_keys(username_field, username)', 'send_keys(password_field, password)',
47
+ 'click(login_button)')
48
+ expect(login).not_to include('.send_keys', '.click')
45
49
  end
46
50
 
47
51
  it 'has private element methods' do
@@ -167,3 +171,70 @@ describe 'Page object content' do
167
171
  include_examples 'watir abstract page', name
168
172
  end
169
173
  end
174
+
175
+ describe 'Page WaitHelper wiring' do
176
+ %w[rspec cucumber].each do |framework|
177
+ %w[selenium ios android cross_platform].each do |automation|
178
+ it "includes WaitHelper in #{framework} #{automation}" do
179
+ page = read_generated("#{framework}_#{automation}", 'page_objects/abstract/page.rb')
180
+ expect(page).to include("require_relative '../../helpers/wait_helper'", 'include WaitHelper')
181
+ end
182
+ end
183
+
184
+ it "does not include WaitHelper in #{framework} watir" do
185
+ expect(read_generated("#{framework}_watir", 'page_objects/abstract/page.rb')).not_to include('WaitHelper')
186
+ end
187
+ end
188
+ end
189
+
190
+ describe 'Abstract page layout' do
191
+ %w[rspec cucumber].each do |framework|
192
+ %w[selenium ios android cross_platform].each do |automation|
193
+ [true, false].each do |examples|
194
+ it "has no blank line before the class end in #{framework} #{automation}#{' (skeleton)' unless examples}" do
195
+ page = read_generated(ensure_project(framework:, automation:, examples:), 'page_objects/abstract/page.rb')
196
+ expect(page).not_to match(/\n\n\s*end\s*\z/)
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ describe 'Example pages on the wait helper' do
204
+ %w[rspec cucumber].each do |framework|
205
+ it "finds the header through the wait helper in #{framework} selenium" do
206
+ page = read_generated("#{framework}_selenium", 'page_objects/abstract/page.rb')
207
+ expect(page).to include("Header.new(find_element(css: '.welcome-msg'))")
208
+ expect(page).not_to include('driver.find_element')
209
+ end
210
+
211
+ %w[ios android cross_platform].each do |automation|
212
+ context "with #{framework} and #{automation}" do
213
+ let(:home) { read_generated("#{framework}_#{automation}", 'page_objects/pages/home.rb') }
214
+ let(:pdp) { read_generated("#{framework}_#{automation}", 'page_objects/pages/pdp.rb') }
215
+
216
+ it 'taps the first product and reads the add to cart text through the wait helper' do
217
+ expect(home).to include('tap(first_product)')
218
+ expect(pdp).to include('text(add_to_cart_button)')
219
+ expect(home + pdp).not_to include('driver.wait', 'driver.find_element')
220
+ end
221
+
222
+ it 'returns locator hashes from the element methods' do
223
+ if automation == 'cross_platform'
224
+ expect(home).to include("{ ios: { accessibility_id: 'product-item-1' },")
225
+ expect(pdp).to include("{ ios: { accessibility_id: 'add-to-cart-button' },")
226
+ expect(home + pdp).not_to include('element(ios:')
227
+ else
228
+ expect(home).to include("{ accessibility_id: 'product-item-1' }")
229
+ expect(pdp).to include("{ accessibility_id: 'add-to-cart-button' }")
230
+ end
231
+ end
232
+
233
+ it 'has valid Ruby syntax' do
234
+ expect(home).to have_valid_ruby_syntax
235
+ expect(pdp).to have_valid_ruby_syntax
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -126,12 +126,12 @@ describe 'Generated Readme content' do
126
126
  'live session tokens', 'must not be committed, cached or uploaded')
127
127
  end
128
128
 
129
- it 'is its own ## section, straight after the Quality gate and before any CI section' do
129
+ it 'is its own ## section, straight before Reporting, so Sponsors does not nest under it' do
130
130
  heading = readme.index("\n## Reusing the login\n")
131
- gate = readme.index("\n### Quality gate\n")
132
- expect(gate).to be < heading
133
- expect(readme[(gate + 1)...heading]).not_to match(/\n##/)
134
- expect(readme[(heading + 1)..][/\n## .*/]).to be_nil.or match(/\A\n## (CI|Azure Pipelines)\z/)
131
+ expect(heading).not_to be_nil
132
+ expect(readme[(heading + 1)..][/\n## .*/]).to eq("\n## Reporting")
133
+ sponsors = readme.index("\n### Sponsors") || readme.size
134
+ expect(readme[0...sponsors].scan(/\n## .*/).last).not_to eq("\n## Reusing the login")
135
135
  end
136
136
  else
137
137
  it 'does not mention login reuse' do
@@ -141,3 +141,124 @@ describe 'Generated Readme content' do
141
141
  end
142
142
  end
143
143
  end
144
+
145
+ describe 'Generated Readme: How the framework works' do
146
+ def section_of(readme)
147
+ readme[/^## How the framework works\n.*?(?=^## )/m]
148
+ end
149
+
150
+ FRAMEWORKS.each do |framework|
151
+ context "with #{framework} and selenium" do
152
+ subject(:readme) { read_generated("#{framework}_selenium", 'Readme.md') }
153
+
154
+ it 'sits between Getting Started and Reusing the login' do
155
+ works = readme.index("\n## How the framework works\n")
156
+ expect(works).not_to be_nil
157
+ expect(readme.index("\n## Getting Started\n")).to be < works
158
+ expect(readme.index("\n## Reusing the login\n")).to be > works
159
+ end
160
+
161
+ it 'covers the web wait helper, chaining, .native, absence checks and how to remove it' do
162
+ section = section_of(readme)
163
+ expect(section).to include('### Removing the wait helper', '.native',
164
+ "find_element(css: '.form').find_element(id: 'email')",
165
+ 'select_by', 'hover', 'drag_and_drop', 'eventually', 'timeout: 0',
166
+ 'find_elements', 'config/config.yml', '[Reporting](#reporting)',
167
+ '[Reusing the login](#reusing-the-login)', 'parallel_tests', 'HEADLESS',
168
+ framework == 'rspec' ? 'RETRY_COUNT' : '--retry 2')
169
+ expect(section).not_to include('scroll_to', 'swipe')
170
+ end
171
+
172
+ it 'explains that the waits inside eventually stretch to its time and are retried' do
173
+ eventually = section_of(readme)[/^### eventually\n.*?(?=^### )/m]
174
+ expect(eventually).to include('a wait inside it that timed out', 'whatever is left of the block',
175
+ '`timeout: 2` gets 2 seconds per try', '(still failing after 10s)')
176
+ end
177
+
178
+ it 'replaces eventually with a true/false Wait block and points at the generators that check' do
179
+ removal = section_of(readme)[/^### Removing the wait helper\n.*/m]
180
+ expect(removal).to include('a plain `expect`, or `Selenium::WebDriver::Wait.new(timeout: 10).until { condition }`',
181
+ 'a block that returns true or false', '`raider g page` and `raider g component`')
182
+ expect(removal).not_to include('raider g spec')
183
+ end
184
+
185
+ it 'lists the five steps to remove the wait helper' do
186
+ removal = section_of(readme)[/^### Removing the wait helper\n.*/m]
187
+ expect(removal).to include('helpers/wait_helper.rb', 'helpers/raider_element.rb', 'include WaitHelper',
188
+ 'driver.find_element(**', 'Selenium::WebDriver::Wait.new', 'implicit_wait',
189
+ 'raider g')
190
+ expect(removal.scan(/^\d\. /).size).to eq(5)
191
+ end
192
+ end
193
+
194
+ %w[ios android cross_platform].each do |automation|
195
+ context "with #{framework} and #{automation}" do
196
+ subject(:readme) { read_generated("#{framework}_#{automation}", 'Readme.md') }
197
+
198
+ it 'sits between Getting Started and Reporting' do
199
+ works = readme.index("\n## How the framework works\n")
200
+ expect(works).not_to be_nil
201
+ expect(readme.index("\n## Getting Started\n")).to be < works
202
+ expect(readme.index("\n## Reporting\n")).to be > works
203
+ end
204
+
205
+ it 'covers the mobile method set, per-OS locators, Appium setup and how to remove the wait helper' do
206
+ section = section_of(readme)
207
+ expect(section).to include('swipe', 'scroll_to', 'tap', 'capabilities.yml', 'raider u start_appium',
208
+ '### Removing the wait helper', '.native', '[Reporting](#reporting)',
209
+ 'timeout: 15', 'http://127.0.0.1:4723')
210
+ expect(section).not_to include('select_by', 'hover', 'drag_and_drop', 'Reusing the login', '/wd/hub')
211
+ expect(section).to include('ios: {') if automation == 'cross_platform'
212
+ end
213
+
214
+ it 'says the device capabilities sit under appium:options' do
215
+ settings = section_of(readme)[/^### Settings\n.*?(?=^### )/m]
216
+ expect(settings).to include('under `appium:options`')
217
+ expect(settings).to include('under the `ios:` and `android:` parts') if automation == 'cross_platform'
218
+ end
219
+ end
220
+ end
221
+
222
+ it "marks the #{framework} watir Readme as deprecated and not the selenium one" do
223
+ watir = read_generated("#{framework}_watir", 'Readme.md')
224
+ expect(watir).to include('> **Deprecated:** Watir is deprecated', 'only available through `-p automation:watir`',
225
+ 'https://github.com/RaiderHQ/ruby_raider#migrating-from-watir')
226
+ expect(read_generated("#{framework}_selenium", 'Readme.md')).not_to include('**Deprecated:**')
227
+ end
228
+
229
+ it "has no such section for #{framework} and watir" do
230
+ expect(read_generated("#{framework}_watir", 'Readme.md')).not_to include('How the framework works')
231
+ end
232
+ end
233
+
234
+ describe 'without example tests' do
235
+ %w[rspec cucumber].product(%w[selenium ios cross_platform]).each do |framework, automation|
236
+ context "with #{framework} and #{automation}" do
237
+ subject(:readme) { read_generated(ensure_project(framework:, automation:, examples: false), 'Readme.md') }
238
+
239
+ it 'still explains how the framework works' do
240
+ expect(readme).to include('## How the framework works', '### The wait helper and RaiderElement')
241
+ end
242
+
243
+ it 'points at no example file' do
244
+ expect(readme).not_to match(/login\.rb|account\.rb|header\.rb|home\.rb|pdp\.rb|users\.yml|model_factory|
245
+ user_factory|login_page_spec|pdp_page_spec|login\.feature|home\.feature|
246
+ login_steps|home_steps|account\ example|files\ in\ this\ project/x)
247
+ end
248
+
249
+ it 'says it has no example tests and how it grows with scaffolding' do
250
+ expect(readme).to include('generated without example tests', 'raider g scaffold checkout')
251
+ end
252
+ end
253
+ end
254
+
255
+ %w[rspec cucumber].each do |framework|
256
+ it "shows how to add the login hook for the project's own login page in #{framework}" do
257
+ readme = read_generated(ensure_project(framework:, automation: 'selenium', examples: false), 'Readme.md')
258
+ section = readme[/^## Reusing the login\n.*?(?=^## )/m]
259
+ hook = framework == 'rspec' ? 'config.before(:each, :logged_in) do' : "Before('@logged_in') do"
260
+ expect(section).to include(hook, 'SessionState.new(driver).reuse(')
261
+ end
262
+ end
263
+ end
264
+ 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,43 +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}/", '')
10
+ def generated_ruby_files(project_name)
11
+ Dir.glob('**/*.rb', base: project_name).sort
12
+ end
16
13
 
17
- it "#{relative} has valid Ruby syntax" do
18
- content = File.read(file)
19
- expect(content).to have_valid_ruby_syntax
20
- end
14
+ def syntax_error_for(content)
15
+ RubyVM::InstructionSequence.compile(content)
16
+ nil
17
+ rescue SyntaxError => e
18
+ e.message
19
+ end
21
20
 
22
- it "#{relative} has frozen_string_literal" do
23
- content = File.read(file)
24
- 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
25
35
  end
26
36
  end
37
+ end
38
+ end
27
39
 
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
-
35
- login_reuse_files.each do |relative|
36
- it "#{relative} is generated as valid Ruby with frozen_string_literal" do
37
- content = File.read(File.join(project_name, relative))
38
- expect(content).to have_valid_ruby_syntax
39
- expect(content).to have_frozen_string_literal
40
- end
41
- 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(')
42
45
  end
43
46
  end
44
47
  end
@@ -173,3 +173,63 @@ describe 'Test file content' do
173
173
  include_examples 'logged in scenario', name
174
174
  end
175
175
  end
176
+
177
+ describe 'Cucumber world WaitHelper wiring' do
178
+ %w[selenium ios android cross_platform].each do |automation|
179
+ it "makes WaitHelper available to steps in #{automation}" do
180
+ world = read_generated("cucumber_#{automation}", 'features/support/world.rb')
181
+ expect(world).to include("require_relative '../../helpers/wait_helper'", 'World(WaitHelper)')
182
+ end
183
+ end
184
+
185
+ it 'leaves the watir world without WaitHelper' do
186
+ expect(read_generated('cucumber_watir', 'features/support/world.rb')).not_to include('WaitHelper')
187
+ end
188
+ end
189
+
190
+ describe 'Example tests on the wait helper' do
191
+ it 'wraps every assertion of the selenium rspec spec in eventually' do
192
+ spec = read_generated('rspec_selenium', 'spec/login_page_spec.rb')
193
+ expect(spec.scan('eventually { expect(').size).to eq(3)
194
+ expect(spec.scan(/^ +expect\(/)).to be_empty
195
+ end
196
+
197
+ it "wraps the selenium cucumber Then step's assertion in eventually" do
198
+ steps = read_generated('cucumber_selenium', 'features/step_definitions/login_steps.rb')
199
+ expect(steps).to include('eventually { expect(account_page.header.customer_name).to eq ')
200
+ end
201
+
202
+ %w[ios android cross_platform].each do |automation|
203
+ it "wraps the #{automation} rspec assertion in eventually" do
204
+ spec = read_generated("rspec_#{automation}", 'spec/pdp_page_spec.rb')
205
+ expect(spec).to include("eventually { expect(pdp_page.add_to_cart_text).to eq 'Add to Cart' }")
206
+ end
207
+
208
+ it "wraps the #{automation} cucumber Then step's assertion in eventually" do
209
+ steps = read_generated("cucumber_#{automation}", 'features/step_definitions/home_steps.rb')
210
+ expect(steps).to include("eventually { expect(pdp_page.add_to_cart_text).to eq 'Add to Cart' }")
211
+ end
212
+ end
213
+ end
214
+
215
+ # The Watir output must stay as master generated it (1274a66), plus the 3.12.1 lint fixes; the snapshots in
216
+ # spec/fixtures/watir_master were generated from that commit's lib.
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) 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.
222
+ describe 'Watir example output' do
223
+ snapshots = File.expand_path('../../fixtures/watir_master', __dir__)
224
+
225
+ { 'rspec_watir' => %w[page_objects/pages/login.rb page_objects/abstract/page.rb spec/login_page_spec.rb Readme.md],
226
+ 'cucumber_watir' => %w[page_objects/pages/login.rb page_objects/abstract/page.rb
227
+ features/step_definitions/login_steps.rb Readme.md] }.each do |project, files|
228
+ files.each do |file|
229
+ it "keeps #{project}/#{file} as master generates it" do
230
+ expected = File.read(File.join(snapshots, project, "#{file}.snapshot"))
231
+ expect(read_generated(project, file)).to eq(expected)
232
+ end
233
+ end
234
+ end
235
+ end