ruby_raider 3.11.0 → 3.12.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 (79) 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/README.md +43 -4
  7. data/lib/commands/utility_commands.rb +1 -1
  8. data/lib/generators/automation/automation_generator.rb +15 -10
  9. data/lib/generators/automation/templates/appium_caps.tt +2 -4
  10. data/lib/generators/automation/templates/home.tt +1 -2
  11. data/lib/generators/automation/templates/login.tt +6 -6
  12. data/lib/generators/automation/templates/page.tt +10 -3
  13. data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
  14. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
  15. data/lib/generators/automation/templates/pdp.tt +3 -2
  16. data/lib/generators/cucumber/cucumber_generator.rb +15 -2
  17. data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
  18. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
  19. data/lib/generators/cucumber/templates/partials/selenium_env.tt +4 -0
  20. data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
  21. data/lib/generators/generator.rb +15 -4
  22. data/lib/generators/helper_generator.rb +8 -0
  23. data/lib/generators/invoke_generators.rb +23 -0
  24. data/lib/generators/menu_generator.rb +26 -14
  25. data/lib/generators/rspec/rspec_generator.rb +10 -3
  26. data/lib/generators/rspec/templates/spec.tt +15 -1
  27. data/lib/generators/template_renderer/template_error.rb +8 -8
  28. data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
  29. data/lib/generators/templates/common/partials/web_config.tt +1 -1
  30. data/lib/generators/templates/common/read_me.tt +508 -24
  31. data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
  32. data/lib/generators/templates/helpers/raider_element.tt +159 -0
  33. data/lib/generators/templates/helpers/session_state.tt +45 -5
  34. data/lib/generators/templates/helpers/spec_helper.tt +9 -1
  35. data/lib/generators/templates/helpers/wait_helper.tt +470 -0
  36. data/lib/ruby_raider/ffmpeg.rb +7 -7
  37. data/lib/ruby_raider.rb +2 -0
  38. data/lib/scaffolding/project_detector.rb +6 -0
  39. data/lib/scaffolding/scaffolding.rb +4 -0
  40. data/lib/scaffolding/templates/component.tt +4 -0
  41. data/lib/scaffolding/templates/page_object.tt +8 -0
  42. data/lib/version +1 -1
  43. data/sig/generators/generator.rbs +2 -0
  44. data/sig/scaffolding/project_detector.rbs +1 -0
  45. data/sig/scaffolding/scaffolding.rbs +1 -0
  46. data/spec/commands/raider_commands_spec.rb +24 -0
  47. data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
  48. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
  49. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +39 -0
  50. data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
  51. data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
  52. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +39 -0
  53. data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
  54. data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
  55. data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
  56. data/spec/integration/commands/utility_commands_spec.rb +8 -0
  57. data/spec/integration/content/config_content_spec.rb +44 -0
  58. data/spec/integration/content/helper_content_spec.rb +79 -0
  59. data/spec/integration/content/page_content_spec.rb +75 -4
  60. data/spec/integration/content/readme_content_spec.rb +126 -5
  61. data/spec/integration/content/syntax_validation_spec.rb +23 -0
  62. data/spec/integration/content/test_content_spec.rb +57 -0
  63. data/spec/integration/end_to_end_spec.rb +50 -28
  64. data/spec/integration/generators/automation_generator_spec.rb +50 -0
  65. data/spec/integration/generators/common_generator_spec.rb +20 -0
  66. data/spec/integration/generators/config_features_spec.rb +2 -2
  67. data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
  68. data/spec/integration/generators/helpers_generator_spec.rb +38 -0
  69. data/spec/integration/generators/invoke_generators_spec.rb +70 -0
  70. data/spec/integration/generators/rspec_generator_spec.rb +20 -0
  71. data/spec/integration/helpers/session_state_spec.rb +46 -0
  72. data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
  73. data/spec/integration/helpers/wait_helper_spec.rb +496 -0
  74. data/spec/integration/helpers/wait_helper_support.rb +183 -0
  75. data/spec/integration/settings_helper.rb +4 -1
  76. data/spec/integration/spec_helper.rb +2 -2
  77. data/spec/menus/menu_generator_spec.rb +64 -2
  78. data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
  79. metadata +15 -2
@@ -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
@@ -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
@@ -31,6 +31,8 @@ describe 'Ruby syntax validation' do
31
31
  else
32
32
  %w[features/support/env.rb features/step_definitions/login_steps.rb]
33
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'
34
36
 
35
37
  login_reuse_files.each do |relative|
36
38
  it "#{relative} is generated as valid Ruby with frozen_string_literal" do
@@ -41,5 +43,26 @@ describe 'Ruby syntax validation' do
41
43
  end
42
44
  end
43
45
  end
46
+
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
65
+ end
66
+ end
44
67
  end
45
68
  end
@@ -173,3 +173,60 @@ 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); 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).
219
+ describe 'Watir example output' do
220
+ snapshots = File.expand_path('../../fixtures/watir_master', __dir__)
221
+
222
+ { 'rspec_watir' => %w[page_objects/pages/login.rb page_objects/abstract/page.rb spec/login_page_spec.rb Readme.md],
223
+ 'cucumber_watir' => %w[page_objects/pages/login.rb page_objects/abstract/page.rb
224
+ features/step_definitions/login_steps.rb Readme.md] }.each do |project, files|
225
+ files.each do |file|
226
+ it "keeps #{project}/#{file} as master generates it" do
227
+ expected = File.read(File.join(snapshots, project, "#{file}.snapshot"))
228
+ expect(read_generated(project, file)).to eq(expected)
229
+ end
230
+ end
231
+ end
232
+ end
@@ -398,6 +398,40 @@ describe 'End-to-End Project Generation and Execution' do
398
398
  end
399
399
  end
400
400
 
401
+ # An empty skeleton (examples: false) still passes its run, so its CI is green until tests are added
402
+ def generate_skeleton_project(framework)
403
+ name = "skeleton_e2e_#{framework}_selenium"
404
+ unless LazyProjectGenerator::GENERATED.include?(name)
405
+ generate_framework(framework:, automation: 'selenium', name:, examples: false)
406
+ LazyProjectGenerator::GENERATED.add(name)
407
+ end
408
+ name
409
+ end
410
+
411
+ context 'Empty skeleton projects (examples: false)' do
412
+ it 'runs bundle exec rspec on a selenium rspec skeleton with 0 examples', :aggregate_failures do
413
+ project = generate_skeleton_project('rspec')
414
+ skip 'Bundle install failed' unless bundle_install(project)
415
+
416
+ ['rspec', 'rspec spec/'].each do |command|
417
+ result = run_in_project(project, "bundle exec #{command}", timeout: 120)
418
+ expect(result[:exit_code]).to eq(0), "#{command}\nSTDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
419
+ expect(result[:stdout]).to include('0 examples, 0 failures')
420
+ end
421
+ end
422
+
423
+ it 'runs bundle exec cucumber on a selenium cucumber skeleton with 0 scenarios', :aggregate_failures do
424
+ project = generate_skeleton_project('cucumber')
425
+ skip 'Bundle install failed' unless bundle_install(project)
426
+
427
+ ['cucumber', 'cucumber features'].each do |command|
428
+ result = run_in_project(project, "bundle exec #{command}", timeout: 120)
429
+ expect(result[:exit_code]).to eq(0), "#{command}\nSTDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
430
+ expect(result[:stdout]).to include('0 scenarios')
431
+ end
432
+ end
433
+ end
434
+
401
435
  # Failure video replay against real browsers. Needs an ffmpeg with libvpx (RAIDER_FFMPEG_PATH), because the
402
436
  # RaiderHQ ffmpeg release has no checksums yet. Skipped when it, or the browser, is missing. Firefox can be
403
437
  # pointed at a specific binary with FIREFOX_BINARY.
@@ -683,38 +717,26 @@ describe 'End-to-End Project Generation and Execution' do
683
717
 
684
718
  # Verify template system performance (caching working)
685
719
  context 'Template System Performance' do
686
- it 'benefits from template caching on second generation' do
720
+ it 'compiles each partial once and reuses it on the second generation' do
687
721
  require_relative '../../lib/generators/generator'
688
722
 
689
- # Clear cache
690
723
  Generator.clear_template_cache
724
+ compiled = []
725
+ allow_any_instance_of(TemplateRenderer::PartialCache).to(
726
+ receive(:compile_template).and_wrap_original { |original, *args| compiled << args.first && original.call(*args) }
727
+ )
691
728
 
692
- # First generation (cache miss)
693
- start_time = Time.now
694
- settings1 = create_settings(framework: 'rspec', automation: 'selenium')
695
- settings1[:name] = 'test_cache_1'
696
- generate_framework(settings1)
697
- first_duration = Time.now - start_time
698
-
699
- # Second generation (cache hit)
700
- start_time = Time.now
701
- settings2 = create_settings(framework: 'rspec', automation: 'selenium')
702
- settings2[:name] = 'test_cache_2'
703
- generate_framework(settings2)
704
- second_duration = Time.now - start_time
705
-
706
- # Second generation should be faster (or similar if I/O dominates)
707
- # At minimum, cache should not slow things down
708
- expect(second_duration).to be <= (first_duration * 1.2)
709
-
710
- # Cleanup
711
- FileUtils.rm_rf('test_cache_1')
712
- FileUtils.rm_rf('test_cache_2')
713
-
714
- # Verify cache has entries
715
- stats = Generator.template_cache_stats
716
- expect(stats[:size]).to be > 0
717
- puts "\nšŸ“Š Template cache: #{stats[:size]} entries, ~#{stats[:memory_estimate] / 1024}KB"
729
+ %w[test_cache_1 test_cache_2].map do |name|
730
+ before = compiled.size
731
+ generate_framework(create_settings(framework: 'rspec', automation: 'selenium').merge(name:))
732
+ compiled.size - before
733
+ end => [first, second]
734
+
735
+ expect(first).to be_positive
736
+ expect(second).to eq(0)
737
+ expect(Generator.template_cache_stats[:size]).to be_positive
738
+ ensure
739
+ FileUtils.rm_rf(%w[test_cache_1 test_cache_2])
718
740
  end
719
741
  end
720
742
  end
@@ -71,4 +71,54 @@ describe AutomationGenerator do
71
71
  context 'with cucumber and appium cross platform' do
72
72
  include_examples 'creates mobile automation framework', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
73
73
  end
74
+
75
+ describe 'without example tests' do
76
+ %w[rspec cucumber].product(%w[selenium ios cross_platform]).each do |framework, automation|
77
+ context "with #{framework} and #{automation}" do
78
+ let(:name) { ensure_project(framework:, automation:, examples: false) }
79
+
80
+ it 'creates no example pages' do
81
+ expect(Dir.glob("#{name}/page_objects/pages/*.rb")).to be_empty
82
+ end
83
+
84
+ it 'keeps the pages folder with a .keep' do
85
+ expect(File).to exist("#{name}/page_objects/pages/.keep")
86
+ end
87
+
88
+ it 'creates the abstract page' do
89
+ expect(File).to exist("#{name}/page_objects/abstract/page.rb")
90
+ end
91
+
92
+ it 'gives the abstract page no header component' do
93
+ expect(File.read("#{name}/page_objects/abstract/page.rb")).not_to include('Header')
94
+ end
95
+
96
+ it 'creates no models' do
97
+ expect(File).not_to exist("#{name}/models")
98
+ end
99
+ end
100
+ end
101
+
102
+ context 'with a web project' do
103
+ let(:name) { ensure_project(framework: 'rspec', automation: 'selenium', examples: false) }
104
+
105
+ it 'creates the abstract component' do
106
+ expect(File).to exist("#{name}/page_objects/abstract/component.rb")
107
+ end
108
+
109
+ it 'keeps the components folder with a .keep and no example component' do
110
+ expect(Dir.children("#{name}/page_objects/components")).to eq(['.keep'])
111
+ end
112
+ end
113
+
114
+ context 'with watir' do
115
+ it 'still creates the examples, because the Watir templates are frozen' do
116
+ InvokeGenerators.generate_framework(framework: 'rspec', automation: 'watir', name: 'skeleton_watir',
117
+ examples: false)
118
+ expect(File).to exist('skeleton_watir/page_objects/pages/login.rb')
119
+ ensure
120
+ FileUtils.rm_rf('skeleton_watir')
121
+ end
122
+ end
123
+ end
74
124
  end
@@ -107,4 +107,24 @@ describe CommonGenerator do
107
107
  include_examples 'creates a config file', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
108
108
  include_examples 'creates a gitignore file', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
109
109
  end
110
+
111
+ describe 'without example tests' do
112
+ %w[rspec cucumber].product(%w[selenium ios cross_platform]).each do |framework, automation|
113
+ context "with #{framework} and #{automation}" do
114
+ let(:name) { ensure_project(framework:, automation:, examples: false) }
115
+ let(:ci_file) { automation == 'selenium' ? 'test_pipeline.yml' : 'appium_pipeline.yml' }
116
+
117
+ it 'creates the README, Gemfile, Rakefile and CI file' do
118
+ %W[Readme.md Gemfile Rakefile .github/workflows/#{ci_file}].each do |file|
119
+ expect(File).to exist("#{name}/#{file}")
120
+ end
121
+ end
122
+
123
+ it 'creates the settings' do
124
+ settings = automation == 'ios' ? 'config/capabilities.yml' : 'config/config.yml'
125
+ expect(File).to exist("#{name}/#{settings}")
126
+ end
127
+ end
128
+ end
129
+ end
110
130
  end
@@ -35,9 +35,9 @@ describe 'Config features for generated projects' do
35
35
  expect(config['driver_options']).to be_nil
36
36
  end
37
37
 
38
- it 'generates driver_helper with timeout from config' do
38
+ it 'generates driver_helper without an implicit wait' do
39
39
  helper = File.read('config_selenium_test/helpers/driver_helper.rb')
40
- expect(helper).to include("config.fetch('timeout', 10)")
40
+ expect(helper).not_to include('implicit_wait')
41
41
  end
42
42
 
43
43
  it 'generates spec_helper with viewport-aware window setup' do