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
@@ -265,11 +265,30 @@ describe 'End-to-End Project Generation and Execution' do
265
265
  FileUtils.rm_f(File.join(project, 'features/step_definitions/reporter_failure_steps.rb'))
266
266
  end
267
267
 
268
+ # Runs the project's own RuboCop with the gems its bundle resolved (newer than this gem's dev bundle).
269
+ # It runs from a copy outside this repo, as a user's project would: inside it RuboCop would also read
270
+ # this repo's .rubocop.yml, gemspec and .ruby-version.
271
+ def expect_project_rubocop_clean(project)
272
+ skip 'Bundle install failed' unless bundle_install(project)
273
+
274
+ Dir.mktmpdir('raider_rubocop') do |tmp|
275
+ copy = File.join(tmp, project)
276
+ FileUtils.cp_r(project, copy)
277
+ puts run_in_project(copy, 'bundle exec rubocop -V', timeout: 60)[:stdout]
278
+ result = run_in_project(copy, 'bundle exec rubocop --cache false', timeout: 180)
279
+ expect(result[:exit_code]).to eq(0), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
280
+ end
281
+ end
282
+
268
283
  # Test Web Frameworks (these can run without external services)
269
284
  context 'Web Automation Frameworks' do
270
285
  describe 'Selenium + RSpec' do
271
286
  include_examples 'executable rspec project', 'rspec_selenium'
272
287
 
288
+ it 'passes its own RuboCop with its bundled gems', :slow do
289
+ expect_project_rubocop_clean('rspec_selenium')
290
+ end
291
+
273
292
  it 'saves the login once and reuses it', :aggregate_failures, :slow do
274
293
  skip 'Bundle install failed' unless bundle_install('rspec_selenium')
275
294
 
@@ -300,6 +319,10 @@ describe 'End-to-End Project Generation and Execution' do
300
319
 
301
320
  describe 'Selenium + Cucumber' do
302
321
  include_examples 'executable cucumber project', 'cucumber_selenium'
322
+
323
+ it 'passes its own RuboCop with its bundled gems', :slow do
324
+ expect_project_rubocop_clean('cucumber_selenium')
325
+ end
303
326
  end
304
327
 
305
328
  describe 'Watir + Cucumber' do
@@ -398,6 +421,40 @@ describe 'End-to-End Project Generation and Execution' do
398
421
  end
399
422
  end
400
423
 
424
+ # An empty skeleton (examples: false) still passes its run, so its CI is green until tests are added
425
+ def generate_skeleton_project(framework)
426
+ name = "skeleton_e2e_#{framework}_selenium"
427
+ unless LazyProjectGenerator::GENERATED.include?(name)
428
+ generate_framework(framework:, automation: 'selenium', name:, examples: false)
429
+ LazyProjectGenerator::GENERATED.add(name)
430
+ end
431
+ name
432
+ end
433
+
434
+ context 'Empty skeleton projects (examples: false)' do
435
+ it 'runs bundle exec rspec on a selenium rspec skeleton with 0 examples', :aggregate_failures do
436
+ project = generate_skeleton_project('rspec')
437
+ skip 'Bundle install failed' unless bundle_install(project)
438
+
439
+ ['rspec', 'rspec spec/'].each do |command|
440
+ result = run_in_project(project, "bundle exec #{command}", timeout: 120)
441
+ expect(result[:exit_code]).to eq(0), "#{command}\nSTDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
442
+ expect(result[:stdout]).to include('0 examples, 0 failures')
443
+ end
444
+ end
445
+
446
+ it 'runs bundle exec cucumber on a selenium cucumber skeleton with 0 scenarios', :aggregate_failures do
447
+ project = generate_skeleton_project('cucumber')
448
+ skip 'Bundle install failed' unless bundle_install(project)
449
+
450
+ ['cucumber', 'cucumber features'].each do |command|
451
+ result = run_in_project(project, "bundle exec #{command}", timeout: 120)
452
+ expect(result[:exit_code]).to eq(0), "#{command}\nSTDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
453
+ expect(result[:stdout]).to include('0 scenarios')
454
+ end
455
+ end
456
+ end
457
+
401
458
  # Failure video replay against real browsers. Needs an ffmpeg with libvpx (RAIDER_FFMPEG_PATH), because the
402
459
  # RaiderHQ ffmpeg release has no checksums yet. Skipped when it, or the browser, is missing. Firefox can be
403
460
  # pointed at a specific binary with FIREFOX_BINARY.
@@ -683,38 +740,26 @@ describe 'End-to-End Project Generation and Execution' do
683
740
 
684
741
  # Verify template system performance (caching working)
685
742
  context 'Template System Performance' do
686
- it 'benefits from template caching on second generation' do
743
+ it 'compiles each partial once and reuses it on the second generation' do
687
744
  require_relative '../../lib/generators/generator'
688
745
 
689
- # Clear cache
690
746
  Generator.clear_template_cache
747
+ compiled = []
748
+ allow_any_instance_of(TemplateRenderer::PartialCache).to(
749
+ receive(:compile_template).and_wrap_original { |original, *args| compiled << args.first && original.call(*args) }
750
+ )
691
751
 
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"
752
+ %w[test_cache_1 test_cache_2].map do |name|
753
+ before = compiled.size
754
+ generate_framework(create_settings(framework: 'rspec', automation: 'selenium').merge(name:))
755
+ compiled.size - before
756
+ end => [first, second]
757
+
758
+ expect(first).to be_positive
759
+ expect(second).to eq(0)
760
+ expect(Generator.template_cache_stats[:size]).to be_positive
761
+ ensure
762
+ FileUtils.rm_rf(%w[test_cache_1 test_cache_2])
718
763
  end
719
764
  end
720
765
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+
5
+ # Ruby only reads the magic comment in the comment block before the first line of code. Ruby would
6
+ # accept it indented, but the generated project's RuboCop (Layout/CommentIndentation) would not.
7
+ RSpec::Matchers.define :have_frozen_string_literal do
8
+ match do |content|
9
+ leading_comments = content.lines.take_while { |line| line.strip.empty? || line.start_with?('#') }
10
+ leading_comments.any? { |line| line.chomp == '# frozen_string_literal: true' }
11
+ end
12
+ failure_message { 'expected an unindented frozen_string_literal magic comment before the first line of code' }
13
+ end
@@ -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
@@ -61,4 +61,40 @@ describe CucumberGenerator do
61
61
  include_examples 'creates cucumber files', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}", 'login'
62
62
  include_examples 'creates factories for web projects', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}", 'login'
63
63
  end
64
+
65
+ describe 'without example tests' do
66
+ %w[selenium ios cross_platform].each do |automation|
67
+ context "with cucumber and #{automation}" do
68
+ let(:name) { ensure_project(framework: 'cucumber', automation:, examples: false) }
69
+
70
+ it 'creates no feature files' do
71
+ expect(Dir.glob("#{name}/features/**/*.feature")).to be_empty
72
+ end
73
+
74
+ it 'keeps an empty step_definitions folder' do
75
+ expect(Dir.children("#{name}/features/step_definitions")).to eq(['.keep'])
76
+ end
77
+
78
+ it 'creates the support files' do
79
+ expect(Dir.children("#{name}/features/support")).to contain_exactly('env.rb', 'world.rb')
80
+ end
81
+
82
+ it 'creates the cucumber.yml file' do
83
+ expect(File).to exist("#{name}/cucumber.yml")
84
+ end
85
+
86
+ it 'creates no user factory' do
87
+ expect(File).not_to exist("#{name}/models/user_factory.rb")
88
+ end
89
+ end
90
+ end
91
+
92
+ it 'creates no accessibility example' do
93
+ InvokeGenerators.generate_framework(framework: 'cucumber', automation: 'selenium', name: 'skeleton_axe_cucumber',
94
+ examples: false, accessibility: true)
95
+ expect(Dir.glob('skeleton_axe_cucumber/features/**/accessibility*')).to be_empty
96
+ ensure
97
+ FileUtils.rm_rf('skeleton_axe_cucumber')
98
+ end
99
+ end
64
100
  end
@@ -32,6 +32,18 @@ describe HelpersGenerator do
32
32
  it 'creates a browser helper file' do
33
33
  expect(File).to exist("#{name}/helpers/browser_helper.rb")
34
34
  end
35
+
36
+ it 'does not create the wait helper or the element wrapper' do
37
+ expect(File).not_to exist("#{name}/helpers/wait_helper.rb")
38
+ expect(File).not_to exist("#{name}/helpers/raider_element.rb")
39
+ end
40
+ end
41
+
42
+ shared_examples 'creates the wait helper' do |name|
43
+ it 'creates the wait helper and the element wrapper' do
44
+ expect(File).to exist("#{name}/helpers/wait_helper.rb")
45
+ expect(File).to exist("#{name}/helpers/raider_element.rb")
46
+ end
35
47
  end
36
48
 
37
49
  shared_examples 'creates rspec helpers' do |name|
@@ -56,6 +68,7 @@ describe HelpersGenerator do
56
68
  include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
57
69
  include_examples 'creates a session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
58
70
  include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
71
+ include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
59
72
  include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
60
73
  end
61
74
 
@@ -70,6 +83,7 @@ describe HelpersGenerator do
70
83
  include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
71
84
  include_examples 'creates a session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
72
85
  include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
86
+ include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
73
87
  include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
74
88
  end
75
89
 
@@ -84,6 +98,7 @@ describe HelpersGenerator do
84
98
  include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
85
99
  include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
86
100
  include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
101
+ include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
87
102
  include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
88
103
  end
89
104
 
@@ -91,6 +106,7 @@ describe HelpersGenerator do
91
106
  include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
92
107
  include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
93
108
  include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
109
+ include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
94
110
  include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
95
111
  end
96
112
 
@@ -98,6 +114,7 @@ describe HelpersGenerator do
98
114
  include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
99
115
  include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
100
116
  include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
117
+ include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
101
118
  include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
102
119
  end
103
120
 
@@ -105,6 +122,7 @@ describe HelpersGenerator do
105
122
  include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
106
123
  include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
107
124
  include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
125
+ include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
108
126
  include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
109
127
  end
110
128
 
@@ -112,6 +130,7 @@ describe HelpersGenerator do
112
130
  include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
113
131
  include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
114
132
  include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
133
+ include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
115
134
  include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
116
135
  include_examples 'creates cross platform helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
117
136
  end
@@ -120,7 +139,26 @@ describe HelpersGenerator do
120
139
  include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
121
140
  include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
122
141
  include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
142
+ include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
123
143
  include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
124
144
  include_examples 'creates cross platform helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
125
145
  end
146
+
147
+ describe 'without example tests' do
148
+ %w[rspec cucumber].product(%w[selenium ios cross_platform]).each do |framework, automation|
149
+ context "with #{framework} and #{automation}" do
150
+ let(:name) { ensure_project(framework:, automation:, examples: false) }
151
+
152
+ it 'creates the wait helper and RaiderElement' do
153
+ expect(File).to exist("#{name}/helpers/wait_helper.rb")
154
+ expect(File).to exist("#{name}/helpers/raider_element.rb")
155
+ end
156
+ end
157
+ end
158
+
159
+ it 'keeps the session state for web' do
160
+ name = ensure_project(framework: 'rspec', automation: 'selenium', examples: false)
161
+ expect(File).to exist("#{name}/helpers/session_state.rb")
162
+ end
163
+ end
126
164
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'tmpdir'
4
5
  require 'open3'
5
6
  require_relative '../spec_helper'
6
7
 
@@ -49,4 +50,133 @@ describe InvokeGenerators do
49
50
  expect(File).not_to exist('case_bad_ci')
50
51
  end
51
52
  end
53
+
54
+ describe '.collect_flags' do
55
+ it 'adds no_examples when examples is false' do
56
+ expect(described_class.collect_flags(automation: 'selenium', examples: false)).to include('no_examples')
57
+ end
58
+
59
+ it 'keeps the examples by default' do
60
+ expect(described_class.collect_flags(automation: 'selenium')).not_to include('no_examples')
61
+ end
62
+
63
+ it 'keeps the examples for watir, whose templates are frozen' do
64
+ expect(described_class.collect_flags(automation: 'watir', examples: false)).not_to include('no_examples')
65
+ end
66
+ end
67
+
68
+ context 'with the Watir deprecation notice' do
69
+ let(:notice) do
70
+ 'Watir is deprecated and will be removed in the next major version of ruby_raider; new projects should use Selenium.'
71
+ end
72
+
73
+ it 'prints it for watir' do
74
+ expect { described_class.generate_framework(framework: 'rspec', automation: 'watir', name: 'case_dep') }
75
+ .to output(/#{Regexp.escape(notice)}/).to_stdout
76
+ end
77
+
78
+ it 'does not print it for selenium' do
79
+ expect { described_class.generate_framework(framework: 'rspec', automation: 'selenium', name: 'case_nodep') }
80
+ .not_to output(/Watir is deprecated/).to_stdout
81
+ end
82
+
83
+ it 'still prints it for raider new with automation:watir' do
84
+ root = File.expand_path('../../..', __dir__)
85
+ out, status = Open3.capture2e('ruby', "#{root}/bin/raider", 'new', 'case_cli_watir', '-p',
86
+ 'framework:rspec', 'automation:watir')
87
+ expect(status.success?).to be(true)
88
+ expect(out).to include(notice)
89
+ expect(File).to exist('case_cli_watir/.rspec')
90
+ end
91
+ end
92
+
93
+ context 'with examples false for watir' do
94
+ it 'says the examples are kept' do
95
+ expect { described_class.generate_framework(framework: 'rspec', automation: 'watir', name: 'case_watir', examples: false) }
96
+ .to output(/Watir projects always include the example tests/).to_stdout
97
+ end
98
+ end
99
+ end
100
+
101
+ describe 'Projects named like a generator option' do
102
+ after { FileUtils.rm_rf(%w[no_examples watir selenium]) }
103
+
104
+ it 'keeps the example tests in a project named no_examples' do
105
+ InvokeGenerators.generate_framework(automation: 'selenium', framework: 'rspec', name: 'no_examples')
106
+ expect(File).to exist('no_examples/spec/login_page_spec.rb')
107
+ expect(File).to exist('no_examples/page_objects/pages/login.rb')
108
+ end
109
+
110
+ it 'gives a Selenium project named watir the wait helper it wires in' do
111
+ InvokeGenerators.generate_framework(automation: 'selenium', framework: 'cucumber', name: 'watir')
112
+ expect(File).to exist('watir/helpers/wait_helper.rb')
113
+ expect(File.read('watir/features/support/env.rb')).not_to include('Watir')
114
+ expect(File.read('watir/page_objects/abstract/page.rb')).to include('include WaitHelper')
115
+ end
116
+
117
+ it 'keeps a Watir project named selenium on Watir' do
118
+ InvokeGenerators.generate_framework(automation: 'watir', framework: 'rspec', name: 'selenium')
119
+ expect(File).not_to exist('selenium/helpers/wait_helper.rb')
120
+ expect(File.read('selenium/page_objects/abstract/page.rb')).to include('browser.element')
121
+ expect(File.read('selenium/page_objects/abstract/page.rb')).not_to include('find_element')
122
+ end
123
+ end
124
+
125
+ describe 'Option-named projects generate what a neutral name generates' do
126
+ option_names = %w[ios android cucumber rspec cross_platform axe_addon selenium watir no_examples azure]
127
+ option_sets = [
128
+ { automation: 'selenium', framework: 'rspec' },
129
+ { automation: 'selenium', framework: 'cucumber', accessibility: true },
130
+ { automation: 'selenium', framework: 'rspec', examples: false, ci: 'azure' },
131
+ { automation: 'watir', framework: 'cucumber' },
132
+ { automation: 'ios', framework: 'rspec' },
133
+ { automation: 'android', framework: 'cucumber' },
134
+ { automation: 'cross_platform', framework: 'rspec' }
135
+ ].freeze
136
+
137
+ def quietly
138
+ result = nil
139
+ expect { result = yield }.to output.to_stdout
140
+ result
141
+ end
142
+
143
+ def snapshot(name, options)
144
+ Dir.mktmpdir do |dir|
145
+ Dir.chdir(dir) do
146
+ InvokeGenerators.generate_framework(options.merge(name:))
147
+ Dir.glob("#{name}/**/*", File::FNM_DOTMATCH).select { |f| File.file?(f) }
148
+ .to_h { |f| [f.delete_prefix("#{name}/"), File.read(f)] }
149
+ end
150
+ end
151
+ end
152
+
153
+ option_sets.each do |options|
154
+ context "with #{options}" do
155
+ let(:neutral) { quietly { snapshot('demo', options) } }
156
+
157
+ option_names.each do |name|
158
+ it "generates the same files for a project named #{name}" do
159
+ actual = quietly { snapshot(name, options) }
160
+ expect(actual.keys.sort).to eq(neutral.keys.sort)
161
+ expect(actual).to eq(neutral)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ describe 'raider new -p examples: validation' do
169
+ after { FileUtils.rm_rf(%w[bad_examples]) }
170
+
171
+ let(:root) { File.expand_path('../../..', __dir__) }
172
+
173
+ it 'rejects an unknown value, names the allowed values and generates nothing' do
174
+ Dir.mktmpdir do |dir|
175
+ _out, err, status = Open3.capture3('ruby', "#{root}/bin/raider", 'new', 'x', '-p',
176
+ 'framework:rspec', 'automation:selenium', 'examples:no', chdir: dir)
177
+ expect(status.exitstatus).to eq(1)
178
+ expect(err).to include('Invalid examples "no". Allowed values: true, false')
179
+ expect(File).not_to exist(File.join(dir, 'x'))
180
+ end
181
+ end
52
182
  end
@@ -46,4 +46,24 @@ describe RspecGenerator do
46
46
  include_examples 'creates rspec files examples',
47
47
  "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}", 'pdp'
48
48
  end
49
+
50
+ describe 'without example tests' do
51
+ %w[selenium ios cross_platform].each do |automation|
52
+ context "with rspec and #{automation}" do
53
+ let(:name) { ensure_project(framework: 'rspec', automation:, examples: false) }
54
+
55
+ it 'creates an empty spec folder' do
56
+ expect(Dir.children("#{name}/spec")).to eq(['.keep'])
57
+ end
58
+ end
59
+ end
60
+
61
+ it 'creates no accessibility example' do
62
+ InvokeGenerators.generate_framework(framework: 'rspec', automation: 'selenium', name: 'skeleton_axe_rspec',
63
+ examples: false, accessibility: true)
64
+ expect(Dir.children('skeleton_axe_rspec/spec')).to eq(['.keep'])
65
+ ensure
66
+ FileUtils.rm_rf('skeleton_axe_rspec')
67
+ end
68
+ end
49
69
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'erb'
4
4
  require 'json'
5
+ require 'timeout'
5
6
  require 'selenium-webdriver'
6
7
  require 'time'
7
8
  require 'tmpdir'
@@ -409,6 +410,51 @@ describe 'SessionState' do
409
410
  expect(Time.iso8601(saved['saved_at'])).to be > before
410
411
  end
411
412
 
413
+ it 'lets concurrent callers restore a fresh login at the same time' do
414
+ write_state('cookies' => [saved_cookie('session')])
415
+ arrived = Queue.new
416
+ checked = Queue.new
417
+ threads = Array.new(2) do
418
+ Thread.new do
419
+ valid = lambda do
420
+ arrived << 1
421
+ Timeout.timeout(2) { sleep 0.01 until arrived.size == 2 }
422
+ checked << 1
423
+ end
424
+ SessionState.new(SessionStateSpec::FakeDriver.new(cookies:)).reuse(valid:) { raise 'logged in' }
425
+ end
426
+ end
427
+ threads.each(&:join)
428
+ expect(checked.size).to eq(2)
429
+ end
430
+
431
+ it 'restores a login another worker saved meanwhile, without a fallback warning' do
432
+ write_state('cookies' => [saved_cookie('session')])
433
+ checks = 0
434
+ valid = lambda do
435
+ checks += 1
436
+ write_state('cookies' => [saved_cookie('session')], 'saved_at' => Time.now.utc.iso8601) if checks == 1
437
+ checks > 1
438
+ end
439
+ logins = 0
440
+ expect { state.reuse(valid:) { logins += 1 } }.not_to output.to_stderr
441
+ expect([logins, checks]).to eq([0, 2])
442
+ end
443
+
444
+ it 'warns once when the second restore after a peer save is also invalid' do
445
+ write_state('cookies' => [saved_cookie('session')])
446
+ checks = 0
447
+ valid = lambda do
448
+ checks += 1
449
+ write_state('cookies' => [saved_cookie('session')], 'saved_at' => Time.now.utc.iso8601) if checks == 1
450
+ false
451
+ end
452
+ logins = 0
453
+ expect { state.reuse(valid:) { logins += 1 } }
454
+ .to output(/\A\[login_state\] login restored from \S+ is no longer valid, logging in again\n\z/).to_stderr
455
+ expect(logins).to eq(1)
456
+ end
457
+
412
458
  it 'serializes concurrent callers on the lock' do
413
459
  count = 0
414
460
  drivers = Array.new(2) { SessionStateSpec::FakeDriver.new(cookies:) }