ruby_raider 3.10.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 (87) 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 +44 -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/feature.tt +5 -0
  18. data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
  19. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
  20. data/lib/generators/cucumber/templates/partials/selenium_env.tt +20 -0
  21. data/lib/generators/cucumber/templates/partials/watir_env.tt +16 -0
  22. data/lib/generators/cucumber/templates/partials/web_steps.tt +8 -0
  23. data/lib/generators/generator.rb +15 -4
  24. data/lib/generators/helper_generator.rb +15 -0
  25. data/lib/generators/invoke_generators.rb +23 -0
  26. data/lib/generators/menu_generator.rb +26 -14
  27. data/lib/generators/rspec/rspec_generator.rb +10 -3
  28. data/lib/generators/rspec/templates/spec.tt +27 -1
  29. data/lib/generators/template_renderer/template_error.rb +8 -8
  30. data/lib/generators/templates/common/git_ignore.tt +4 -1
  31. data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
  32. data/lib/generators/templates/common/partials/web_config.tt +5 -2
  33. data/lib/generators/templates/common/read_me.tt +508 -0
  34. data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
  35. data/lib/generators/templates/helpers/raider_element.tt +159 -0
  36. data/lib/generators/templates/helpers/session_state.tt +246 -0
  37. data/lib/generators/templates/helpers/spec_helper.tt +28 -0
  38. data/lib/generators/templates/helpers/wait_helper.tt +470 -0
  39. data/lib/ruby_raider/ffmpeg.rb +7 -7
  40. data/lib/ruby_raider.rb +2 -0
  41. data/lib/scaffolding/project_detector.rb +6 -0
  42. data/lib/scaffolding/scaffolding.rb +4 -0
  43. data/lib/scaffolding/templates/component.tt +4 -0
  44. data/lib/scaffolding/templates/page_object.tt +8 -0
  45. data/lib/version +1 -1
  46. data/ruby_raider.gemspec +1 -0
  47. data/sig/generators/generator.rbs +2 -0
  48. data/sig/scaffolding/project_detector.rbs +1 -0
  49. data/sig/scaffolding/scaffolding.rbs +1 -0
  50. data/spec/commands/raider_commands_spec.rb +24 -0
  51. data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
  52. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
  53. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +39 -0
  54. data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
  55. data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
  56. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +39 -0
  57. data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
  58. data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
  59. data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
  60. data/spec/integration/commands/utility_commands_spec.rb +8 -0
  61. data/spec/integration/content/azure_content_spec.rb +4 -0
  62. data/spec/integration/content/ci_content_spec.rb +11 -0
  63. data/spec/integration/content/common_content_spec.rb +27 -0
  64. data/spec/integration/content/config_content_spec.rb +62 -0
  65. data/spec/integration/content/cucumber_reporter_content_spec.rb +25 -0
  66. data/spec/integration/content/helper_content_spec.rb +111 -0
  67. data/spec/integration/content/page_content_spec.rb +75 -4
  68. data/spec/integration/content/readme_content_spec.rb +149 -0
  69. data/spec/integration/content/syntax_validation_spec.rb +38 -0
  70. data/spec/integration/content/test_content_spec.rb +82 -0
  71. data/spec/integration/end_to_end_spec.rb +73 -28
  72. data/spec/integration/generators/automation_generator_spec.rb +50 -0
  73. data/spec/integration/generators/common_generator_spec.rb +20 -0
  74. data/spec/integration/generators/config_features_spec.rb +2 -2
  75. data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
  76. data/spec/integration/generators/helpers_generator_spec.rb +60 -0
  77. data/spec/integration/generators/invoke_generators_spec.rb +70 -0
  78. data/spec/integration/generators/rspec_generator_spec.rb +20 -0
  79. data/spec/integration/helpers/session_state_spec.rb +474 -0
  80. data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
  81. data/spec/integration/helpers/wait_helper_spec.rb +496 -0
  82. data/spec/integration/helpers/wait_helper_support.rb +183 -0
  83. data/spec/integration/settings_helper.rb +4 -1
  84. data/spec/integration/spec_helper.rb +2 -2
  85. data/spec/menus/menu_generator_spec.rb +64 -2
  86. data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
  87. metadata +31 -2
@@ -139,6 +139,21 @@ describe 'Common file content' do
139
139
  end
140
140
  end
141
141
 
142
+ shared_examples 'web gitignore for saved logins' do |project_name|
143
+ subject(:lines) { read_generated(project_name, '.gitignore').lines.map(&:strip) }
144
+
145
+ it 'ignores .raider/auth/ but not the whole .raider/' do
146
+ expect(lines).to include('.raider/auth/')
147
+ expect(lines).not_to include('.raider/', '.raider')
148
+ end
149
+ end
150
+
151
+ shared_examples 'mobile gitignore without saved logins' do |project_name|
152
+ it 'has no .raider line' do
153
+ expect(read_generated(project_name, '.gitignore')).not_to include('.raider')
154
+ end
155
+ end
156
+
142
157
  # --- Spec helper retry config (C4) ---
143
158
 
144
159
  shared_examples 'spec helper with retry config' do |project_name|
@@ -207,6 +222,7 @@ describe 'Common file content' do
207
222
  it_behaves_like 'rspec gitignore', name
208
223
  it_behaves_like 'spec helper with retry config', name
209
224
  it_behaves_like 'gemfile with rspec-retry', name
225
+ it_behaves_like 'web gitignore for saved logins', name
210
226
  end
211
227
 
212
228
  context 'with rspec and watir' do
@@ -217,6 +233,7 @@ describe 'Common file content' do
217
233
  it_behaves_like 'rspec gitignore', name
218
234
  it_behaves_like 'spec helper with retry config', name
219
235
  it_behaves_like 'gemfile with rspec-retry', name
236
+ it_behaves_like 'web gitignore for saved logins', name
220
237
  end
221
238
 
222
239
  # --- Cucumber contexts ---
@@ -229,6 +246,7 @@ describe 'Common file content' do
229
246
  it_behaves_like 'non-rspec gitignore', name
230
247
  it_behaves_like 'gemfile without rspec-retry', name
231
248
  it_behaves_like 'cucumber config with retry', name
249
+ it_behaves_like 'web gitignore for saved logins', name
232
250
  end
233
251
 
234
252
  context 'with cucumber and watir' do
@@ -239,5 +257,14 @@ describe 'Common file content' do
239
257
  it_behaves_like 'non-rspec gitignore', name
240
258
  it_behaves_like 'gemfile without rspec-retry', name
241
259
  it_behaves_like 'cucumber config with retry', name
260
+ it_behaves_like 'web gitignore for saved logins', name
261
+ end
262
+
263
+ %w[android ios cross_platform].each do |automation|
264
+ %w[rspec cucumber].each do |framework|
265
+ context "with #{framework} and #{automation}" do
266
+ it_behaves_like 'mobile gitignore without saved logins', "#{framework}_#{automation}"
267
+ end
268
+ end
242
269
  end
243
270
  end
@@ -59,6 +59,13 @@ describe 'Config file content' do
59
59
  end
60
60
  end
61
61
 
62
+ shared_examples 'login state config' do |project_name|
63
+ it 'has a login_state max_age of an hour' do
64
+ config = YAML.safe_load(read_generated(project_name, 'config/config.yml'), permitted_classes: [Symbol])
65
+ expect(config['login_state']).to eq('max_age' => 3600)
66
+ end
67
+ end
68
+
62
69
  # --- Web config per automation ---
63
70
 
64
71
  context 'with rspec and selenium' do
@@ -66,18 +73,21 @@ describe 'Config file content' do
66
73
  include_examples 'valid web config', name
67
74
  include_examples 'selenium config with timeout', name
68
75
  include_examples 'video config', name
76
+ include_examples 'login state config', name
69
77
  end
70
78
 
71
79
  context 'with rspec and watir' do
72
80
  name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
73
81
  include_examples 'valid web config', name
74
82
  include_examples 'config without driver_options', name
83
+ include_examples 'login state config', name
75
84
  end
76
85
 
77
86
  context 'with cucumber and selenium' do
78
87
  name = "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
79
88
  include_examples 'valid web config', name
80
89
  include_examples 'selenium config with timeout', name
90
+ include_examples 'login state config', name
81
91
  end
82
92
 
83
93
  # --- Mobile config ---
@@ -100,4 +110,56 @@ describe 'Config file content' do
100
110
  expect(caps).to match(/platformName/i)
101
111
  end
102
112
  end
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
+
131
+ %w[android ios cross_platform].each do |automation|
132
+ it "has no login_state in the #{automation} config" do
133
+ Dir.glob(File.join("#{FrameworkIndex::RSPEC}_#{automation}", 'config', '*.yml')).each do |file|
134
+ expect(File.read(file)).not_to include('login_state')
135
+ end
136
+ end
137
+ end
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
103
165
  end
@@ -83,3 +83,28 @@ describe 'Cucumber raider_reporter wiring' do
83
83
  end
84
84
  end
85
85
  end
86
+
87
+ describe 'Cucumber login reuse wiring' do
88
+ { 'selenium' => 'driver', 'watir' => 'browser.driver' }.each do |automation, target|
89
+ context "with cucumber and #{automation}" do
90
+ let(:env) { read_generated("#{FrameworkIndex::CUCUMBER}_#{automation}", 'features/support/env.rb') }
91
+
92
+ it 'requires session_state' do
93
+ expect(env).to include("require_relative '../../helpers/session_state'")
94
+ end
95
+
96
+ it "reuses the saved login in Before('@logged_in'), defined after the plain Before" do
97
+ expect(env).to include("Before('@logged_in') do", "SessionState.new(#{target}).reuse(",
98
+ "UserFactory.for('registered user')")
99
+ expect(env.index("Before('@logged_in')")).to be > env.index(/^Before do/)
100
+ end
101
+ end
102
+ end
103
+
104
+ %w[android ios cross_platform].each do |automation|
105
+ it "does not mention session_state with cucumber and #{automation}" do
106
+ env = read_generated("#{FrameworkIndex::CUCUMBER}_#{automation}", 'features/support/env.rb')
107
+ expect(env).not_to include('session_state')
108
+ end
109
+ end
110
+ end
@@ -365,4 +365,115 @@ describe 'Helper file content' do
365
365
  end
366
366
  end
367
367
  end
368
+
369
+ # --- Opt-in login reuse ---
370
+
371
+ { 'selenium' => 'driver', 'watir' => 'browser.driver' }.each do |automation, target|
372
+ context "with rspec and #{automation} login reuse" do
373
+ subject(:helper) { read_generated("#{FrameworkIndex::RSPEC}_#{automation}", 'helpers/spec_helper.rb') }
374
+
375
+ it 'requires session_state and opts in through the :logged_in tag' do
376
+ expect(helper).to include("require_relative 'session_state'", 'config.before(:each, :logged_in)')
377
+ end
378
+
379
+ it 'reuses the saved login for the driver, falling back to the real login' do
380
+ expect(helper).to include("SessionState.new(#{target}).reuse(")
381
+ expect(helper).to include("user = ModelFactory.for('users')['registered user']", ".login(user['username'], user['password'])")
382
+ expect(helper).to include("header.customer_name == \"Welcome back \#{user['name']}\"")
383
+ end
384
+
385
+ it 'requires the models and pages the login needs' do
386
+ expect(helper).to include("require_relative '../models/model_factory'",
387
+ "require_relative '../page_objects/pages/login'",
388
+ "require_relative '../page_objects/pages/account'")
389
+ end
390
+ end
391
+ end
392
+
393
+ %w[android ios cross_platform].each do |automation|
394
+ context "with rspec and #{automation} without login reuse" do
395
+ it 'does not mention session_state' do
396
+ expect(read_generated("#{FrameworkIndex::RSPEC}_#{automation}", 'helpers/spec_helper.rb')).not_to include('session_state')
397
+ end
398
+ end
399
+ end
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
368
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
@@ -112,4 +112,153 @@ describe 'Generated Readme content' do
112
112
  end
113
113
  end
114
114
  end
115
+
116
+ FRAMEWORKS.product(AUTOMATION_TYPES).each do |framework, automation|
117
+ context "with #{framework} and #{automation}, the login reuse section" do
118
+ subject(:readme) { read_generated("#{framework}_#{automation}", 'Readme.md') }
119
+
120
+ if %w[selenium watir].include?(automation)
121
+ it 'documents the tags, other roles, max_age, the env switch and the secrets warning' do
122
+ section = readme[/^## Reusing the login\n.*?(?=^##)/m].gsub(/\s+/, ' ')
123
+ expect(section).to include('`:logged_in`', '`@logged_in`', "SessionState.new(driver, name: 'admin')",
124
+ 'browser.driver', 'login_state', 'max_age', 'RAIDER_LOGIN_STATE=off',
125
+ 'RAIDER_LOGIN_STATE=refresh', '.raider/auth/', 'git-ignored',
126
+ 'live session tokens', 'must not be committed, cached or uploaded')
127
+ end
128
+
129
+ it 'is its own ## section, straight before Reporting, so Sponsors does not nest under it' do
130
+ heading = readme.index("\n## Reusing the login\n")
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
+ end
136
+ else
137
+ it 'does not mention login reuse' do
138
+ expect(readme).not_to include('Reusing the login', 'RAIDER_LOGIN_STATE', '.raider/auth/')
139
+ end
140
+ end
141
+ end
142
+ end
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
115
264
  end
@@ -24,6 +24,44 @@ describe 'Ruby syntax validation' do
24
24
  expect(content).to have_frozen_string_literal
25
25
  end
26
26
  end
27
+
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'
36
+
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
42
+ end
43
+ end
44
+ end
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
27
65
  end
28
66
  end
29
67
  end