ruby_raider 3.0.0 → 3.0.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.
- checksums.yaml +4 -4
- data/lib/commands/scaffolding_commands.rb +26 -1
- data/lib/commands/utility_commands.rb +10 -1
- data/lib/generators/templates/helpers/browser_helper.tt +1 -1
- data/lib/generators/templates/helpers/capybara_helper.tt +1 -1
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -1
- data/lib/scaffolding/project_detector.rb +28 -4
- data/lib/utilities/utilities.rb +10 -2
- data/lib/version +1 -1
- data/spec/commands/scaffolding_commands_spec.rb +22 -0
- data/spec/integration/commands/utility_commands_spec.rb +8 -8
- data/spec/integration/scaffolding_e2e_spec.rb +775 -0
- data/spec/scaffolding/scaffold_project_detector_spec.rb +38 -0
- data/spec/utilities/headless_config_spec.rb +89 -0
- data/spec/utilities/utilities_spec.rb +105 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ef04718aea3c6036141557edd000ddc0ecb4df784bbfa6434ddf7a275edb73f
|
|
4
|
+
data.tar.gz: 1ba77ec6eeb15fe8520819dce31dde3b4a8763b83d901ddf32a4b7ca47411cdc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a78ba73874f84e22f137f7c996a44fbe5703b80527422966bb31ecb83e1dfc0e21442edcfdcc1bf015bcc7129a4683a711d10c4c1c78da6f362ac7faf70b1230
|
|
7
|
+
data.tar.gz: eaac9b04da1d97f865ba8ebee4643bb9a594ffbaf884aa905b204ed2e1759d5189a560d3e1165721af4fa60de4c603a1e7be31572e5ebbc53eeace5d60b4a7d4
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'pathname'
|
|
5
6
|
require_relative '../generators/menu_generator'
|
|
6
7
|
require_relative '../scaffolding/scaffolding'
|
|
7
8
|
require_relative '../scaffolding/name_normalizer'
|
|
@@ -165,7 +166,27 @@ class ScaffoldingCommands < Thor
|
|
|
165
166
|
|
|
166
167
|
no_commands do
|
|
167
168
|
def load_config_path(type)
|
|
168
|
-
|
|
169
|
+
ScaffoldProjectDetector.config_path(type)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def validate_project!
|
|
173
|
+
warnings = ScaffoldProjectDetector.validate_project
|
|
174
|
+
warnings.each { |w| say "Warning: #{w}", :yellow }
|
|
175
|
+
show_detection_defaults
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def show_detection_defaults
|
|
179
|
+
gemfile = ScaffoldProjectDetector.read_gemfile
|
|
180
|
+
if gemfile.empty?
|
|
181
|
+
say 'Warning: No Gemfile found, defaulting to selenium + rspec templates.', :yellow
|
|
182
|
+
else
|
|
183
|
+
automation = ScaffoldProjectDetector.detect_automation(gemfile)
|
|
184
|
+
framework = ScaffoldProjectDetector.detect_framework(gemfile)
|
|
185
|
+
if automation.nil?
|
|
186
|
+
say 'Warning: Could not detect automation library from Gemfile, defaulting to selenium.', :yellow
|
|
187
|
+
end
|
|
188
|
+
say 'Warning: Could not detect test framework from Gemfile, defaulting to rspec.', :yellow if framework.nil?
|
|
189
|
+
end
|
|
169
190
|
end
|
|
170
191
|
|
|
171
192
|
def delete_scaffolding(name, type)
|
|
@@ -186,6 +207,7 @@ class ScaffoldingCommands < Thor
|
|
|
186
207
|
end
|
|
187
208
|
|
|
188
209
|
def generate_default_scaffold(name)
|
|
210
|
+
validate_project!
|
|
189
211
|
uses = options[:uses]
|
|
190
212
|
if Pathname.new('spec').exist? && !Pathname.new('features').exist?
|
|
191
213
|
generate_scaffolding(name, 'spec', load_config_path('spec'), uses:)
|
|
@@ -197,6 +219,7 @@ class ScaffoldingCommands < Thor
|
|
|
197
219
|
end
|
|
198
220
|
|
|
199
221
|
def generate_selected_components(name, components)
|
|
222
|
+
validate_project!
|
|
200
223
|
uses = options[:uses]
|
|
201
224
|
components.each do |comp|
|
|
202
225
|
comp = comp.downcase.strip
|
|
@@ -211,6 +234,7 @@ class ScaffoldingCommands < Thor
|
|
|
211
234
|
|
|
212
235
|
def generate_crud(name)
|
|
213
236
|
require_relative '../scaffolding/crud_generator'
|
|
237
|
+
validate_project!
|
|
214
238
|
if options[:dry_run]
|
|
215
239
|
crud = CrudGenerator.new(name, Scaffolding, method(:load_config_path))
|
|
216
240
|
DryRunPresenter.preview(crud.planned_files)
|
|
@@ -266,6 +290,7 @@ class ScaffoldingCommands < Thor
|
|
|
266
290
|
|
|
267
291
|
def interactive_scaffold
|
|
268
292
|
require_relative '../scaffolding/scaffold_menu'
|
|
293
|
+
validate_project!
|
|
269
294
|
result = ScaffoldMenu.new.run
|
|
270
295
|
return unless result
|
|
271
296
|
|
|
@@ -35,7 +35,7 @@ class UtilityCommands < Thor
|
|
|
35
35
|
browser_options(selected_options) if selected_options || options[:delete]
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
desc 'browser_options [OPTIONS]', 'Sets the browser
|
|
38
|
+
desc 'browser_options [OPTIONS]', 'Sets the browser arguments for the current browser (e.g. no-sandbox, headless)'
|
|
39
39
|
option :delete,
|
|
40
40
|
type: :boolean, required: false, desc: 'This will delete your browser options', aliases: '-d'
|
|
41
41
|
|
|
@@ -44,6 +44,15 @@ class UtilityCommands < Thor
|
|
|
44
44
|
Utilities.delete_browser_options if options[:delete]
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
desc 'headless [on/off]', 'Toggles headless mode for browser tests'
|
|
48
|
+
|
|
49
|
+
def headless(toggle)
|
|
50
|
+
enabled = %w[on true 1 yes].include?(toggle.downcase)
|
|
51
|
+
Utilities.headless = enabled
|
|
52
|
+
state = enabled ? 'enabled' : 'disabled'
|
|
53
|
+
say "Headless mode #{state}", :green
|
|
54
|
+
end
|
|
55
|
+
|
|
47
56
|
desc 'raid', 'It runs all the tests in a project'
|
|
48
57
|
option :parallel,
|
|
49
58
|
type: :boolean, required: false, desc: 'It runs the tests in parallel', aliases: '-p'
|
|
@@ -20,7 +20,7 @@ module BrowserHelper
|
|
|
20
20
|
Watir.default_timeout = config.fetch('timeout', 10)
|
|
21
21
|
browser_name = config['browser'].to_s
|
|
22
22
|
args = args.empty? ? config['browser_arguments'][browser_name] : args
|
|
23
|
-
args += ['--headless'] if ENV['HEADLESS'] && !args.include?('--headless')
|
|
23
|
+
args += ['--headless'] if (ENV['HEADLESS'] || config['headless']) && !args.include?('--headless')
|
|
24
24
|
capitalized = browser_name == 'ie' ? browser_name.upcase : browser_name.capitalize
|
|
25
25
|
browser_options = "Selenium::WebDriver::#{capitalized}::Options".constantize.new(args:)
|
|
26
26
|
debug_cfg = config['debug'] || {}
|
|
@@ -20,7 +20,7 @@ module CapybaraHelper
|
|
|
20
20
|
Capybara.register_driver :selenium do |app|
|
|
21
21
|
browser = config['browser'].to_sym
|
|
22
22
|
args = config['browser_arguments'][config['browser']] || []
|
|
23
|
-
args += ['--headless'] if ENV['HEADLESS'] && !args.include?('--headless')
|
|
23
|
+
args += ['--headless'] if (ENV['HEADLESS'] || config['headless']) && !args.include?('--headless')
|
|
24
24
|
options = Selenium::WebDriver::Chrome::Options.new(args:)
|
|
25
25
|
debug_cfg = config['debug'] || {}
|
|
26
26
|
if debug_cfg.fetch('enabled', false) || ENV['DEBUG']&.downcase == 'true'
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
def browser_arguments(*opts)
|
|
10
10
|
args = opts.empty? ? @config['browser_arguments'][@config['browser']] : opts
|
|
11
|
-
args += ['--headless'] if ENV['HEADLESS'] && !args.include?('--headless')
|
|
11
|
+
args += ['--headless'] if (ENV['HEADLESS'] || @config['headless']) && !args.include?('--headless')
|
|
12
12
|
args
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
3
5
|
module ScaffoldProjectDetector
|
|
4
6
|
AUTOMATION_GEMS = {
|
|
5
7
|
'capybara' => 'capybara',
|
|
@@ -17,6 +19,10 @@ module ScaffoldProjectDetector
|
|
|
17
19
|
'minitest' => 'minitest'
|
|
18
20
|
}.freeze
|
|
19
21
|
|
|
22
|
+
CONFIG_PATH = 'config/config.yml'
|
|
23
|
+
|
|
24
|
+
class Error < StandardError; end
|
|
25
|
+
|
|
20
26
|
module_function
|
|
21
27
|
|
|
22
28
|
def detect
|
|
@@ -62,11 +68,29 @@ module ScaffoldProjectDetector
|
|
|
62
68
|
detect_automation == 'watir'
|
|
63
69
|
end
|
|
64
70
|
|
|
71
|
+
# Validates that the current directory looks like a Ruby Raider project.
|
|
72
|
+
# Returns an array of warning messages (empty = valid project).
|
|
73
|
+
def validate_project
|
|
74
|
+
warnings = []
|
|
75
|
+
warnings << 'Gemfile not found. Are you in a Ruby Raider project directory?' unless File.exist?('Gemfile')
|
|
76
|
+
unless File.exist?(CONFIG_PATH)
|
|
77
|
+
warnings << "#{CONFIG_PATH} not found. Scaffolded pages won't include URL navigation methods."
|
|
78
|
+
end
|
|
79
|
+
warnings
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Loads and parses config/config.yml. Returns a hash.
|
|
83
|
+
# Raises ScaffoldProjectDetector::Error on malformed YAML.
|
|
65
84
|
def config
|
|
66
|
-
return {} unless File.exist?(
|
|
85
|
+
return {} unless File.exist?(CONFIG_PATH)
|
|
86
|
+
|
|
87
|
+
YAML.safe_load(File.read(CONFIG_PATH), permitted_classes: [Symbol]) || {}
|
|
88
|
+
rescue Psych::SyntaxError => e
|
|
89
|
+
raise Error, "#{CONFIG_PATH} has invalid YAML syntax: #{e.message}"
|
|
90
|
+
end
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{}
|
|
92
|
+
# Returns the custom path for a scaffold type from config, or nil.
|
|
93
|
+
def config_path(type)
|
|
94
|
+
config["#{type}_path"]
|
|
71
95
|
end
|
|
72
96
|
end
|
data/lib/utilities/utilities.rb
CHANGED
|
@@ -44,14 +44,22 @@ module Utilities
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def browser_options=(opts)
|
|
47
|
-
|
|
47
|
+
browser_name = config['browser'] || 'chrome'
|
|
48
|
+
config['browser_arguments'] ||= {}
|
|
49
|
+
config['browser_arguments'][browser_name] = Array(opts).flatten
|
|
50
|
+
overwrite_yaml
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def delete_browser_options
|
|
51
|
-
config
|
|
54
|
+
browser_name = config['browser'] || 'chrome'
|
|
55
|
+
config['browser_arguments']&.delete(browser_name)
|
|
52
56
|
overwrite_yaml
|
|
53
57
|
end
|
|
54
58
|
|
|
59
|
+
def headless=(enabled)
|
|
60
|
+
set('headless', enabled)
|
|
61
|
+
end
|
|
62
|
+
|
|
55
63
|
def llm_provider=(provider)
|
|
56
64
|
set('llm_provider', provider)
|
|
57
65
|
end
|
data/lib/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.0.
|
|
1
|
+
3.0.1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'ScaffoldingCommands' do
|
|
6
|
+
describe 'dependencies' do
|
|
7
|
+
it 'loads without relying on transitive requires for Pathname' do
|
|
8
|
+
# Loads the file in a clean Ruby process where nothing else has required 'pathname'.
|
|
9
|
+
# This catches the exact bug where Pathname is used but never explicitly required,
|
|
10
|
+
# which works in test suites (rspec loads pathname transitively) but crashes at runtime.
|
|
11
|
+
script = <<~RUBY
|
|
12
|
+
require_relative '../../lib/commands/scaffolding_commands'
|
|
13
|
+
puts 'OK'
|
|
14
|
+
RUBY
|
|
15
|
+
|
|
16
|
+
stdout, stderr, status = Open3.capture3('ruby', '-e', script, chdir: __dir__)
|
|
17
|
+
expect(status.success?).to be(true),
|
|
18
|
+
"ScaffoldingCommands failed to load in isolation:\n#{stderr}"
|
|
19
|
+
expect(stdout.strip).to eq('OK')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -43,10 +43,10 @@ describe UtilityCommands do
|
|
|
43
43
|
expect(config['browser']).to eql ':firefox'
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
it 'updates the browser
|
|
46
|
+
it 'updates the browser arguments for the current browser' do
|
|
47
47
|
utility.new.invoke(:browser, nil, %w[:firefox --opts headless start-maximized start-fullscreen])
|
|
48
48
|
config = YAML.load_file('config/config.yml')
|
|
49
|
-
expect(config['
|
|
49
|
+
expect(config['browser_arguments'][':firefox']).to eql %w[headless start-maximized start-fullscreen]
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -91,23 +91,23 @@ describe UtilityCommands do
|
|
|
91
91
|
expect(config['feature_path']).to eql path
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
it 'updates
|
|
94
|
+
it 'updates the browser arguments for the current browser' do
|
|
95
95
|
utility.new.invoke(:browser, nil, %w[:firefox --opts headless])
|
|
96
96
|
config = YAML.load_file('config/config.yml')
|
|
97
|
-
expect(config['
|
|
97
|
+
expect(config['browser_arguments'][':firefox']).to eql %w[headless]
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
it 'deletes the browser
|
|
100
|
+
it 'deletes the browser arguments when passed with the delete parameter' do
|
|
101
101
|
utility.new.invoke(:browser, nil, %w[:firefox --opts headless --delete])
|
|
102
102
|
config = YAML.load_file('config/config.yml')
|
|
103
|
-
expect(config
|
|
103
|
+
expect(config.dig('browser_arguments', ':firefox')).to be_nil
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
it 'deletes the browser
|
|
106
|
+
it 'deletes the browser arguments' do
|
|
107
107
|
utility.new.invoke(:browser, nil, %w[:firefox --opts headless])
|
|
108
108
|
utility.new.invoke(:browser, nil, %w[--delete])
|
|
109
109
|
config = YAML.load_file('config/config.yml')
|
|
110
|
-
expect(config
|
|
110
|
+
expect(config.dig('browser_arguments', ':firefox')).to be_nil
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
113
|
end
|
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require_relative '../../lib/commands/scaffolding_commands'
|
|
6
|
+
require_relative '../../lib/scaffolding/scaffolding'
|
|
7
|
+
|
|
8
|
+
# Custom matchers (defined inline to avoid pulling in integration spec_helper)
|
|
9
|
+
RSpec::Matchers.define :have_frozen_string_literal do
|
|
10
|
+
match { |content| content.include?('# frozen_string_literal: true') }
|
|
11
|
+
failure_message { 'expected file to contain frozen_string_literal magic comment' }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
RSpec::Matchers.define :have_valid_ruby_syntax do
|
|
15
|
+
match do |content|
|
|
16
|
+
RubyVM::InstructionSequence.compile(content)
|
|
17
|
+
true
|
|
18
|
+
rescue SyntaxError
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
failure_message { |_content| "expected valid Ruby syntax but got SyntaxError: #{$ERROR_INFO&.message}" }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# End-to-end scaffolding tests that verify the scaffold command creates the right
|
|
25
|
+
# files with the right content across all automation × framework combinations.
|
|
26
|
+
describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
|
|
27
|
+
|
|
28
|
+
WEB_AUTOMATIONS = %w[selenium capybara watir].freeze
|
|
29
|
+
TEST_FRAMEWORKS = %w[rspec cucumber minitest].freeze
|
|
30
|
+
|
|
31
|
+
# The scaffolding system's generate_default_scaffold checks for spec/ or features/.
|
|
32
|
+
# Minitest projects have test/ only — so the scaffold falls through to feature+steps.
|
|
33
|
+
def self.scaffold_style(framework)
|
|
34
|
+
case framework
|
|
35
|
+
when 'rspec' then :spec
|
|
36
|
+
when 'cucumber' then :feature
|
|
37
|
+
when 'minitest' then :feature # no spec/ dir → falls to else clause
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Build a minimal mock project that the scaffolding system can detect.
|
|
42
|
+
def self.build_mock_project(automation, framework) # rubocop:disable Metrics/MethodLength
|
|
43
|
+
dir = File.expand_path("tmp_scaffold_e2e_#{framework}_#{automation}")
|
|
44
|
+
FileUtils.rm_rf(dir)
|
|
45
|
+
FileUtils.mkdir_p("#{dir}/page_objects/pages")
|
|
46
|
+
FileUtils.mkdir_p("#{dir}/page_objects/abstract")
|
|
47
|
+
FileUtils.mkdir_p("#{dir}/page_objects/components")
|
|
48
|
+
FileUtils.mkdir_p("#{dir}/helpers")
|
|
49
|
+
FileUtils.mkdir_p("#{dir}/config")
|
|
50
|
+
FileUtils.mkdir_p("#{dir}/models/data")
|
|
51
|
+
|
|
52
|
+
case framework
|
|
53
|
+
when 'rspec' then FileUtils.mkdir_p("#{dir}/spec")
|
|
54
|
+
when 'cucumber' then FileUtils.mkdir_p("#{dir}/features/step_definitions")
|
|
55
|
+
FileUtils.mkdir_p("#{dir}/features/support")
|
|
56
|
+
when 'minitest' then FileUtils.mkdir_p("#{dir}/test")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
gems = []
|
|
60
|
+
case automation
|
|
61
|
+
when 'selenium' then gems << "gem 'selenium-webdriver'"
|
|
62
|
+
when 'capybara' then gems << "gem 'capybara'"
|
|
63
|
+
when 'watir' then gems << "gem 'watir'"
|
|
64
|
+
end
|
|
65
|
+
case framework
|
|
66
|
+
when 'rspec' then gems << "gem 'rspec'"
|
|
67
|
+
when 'cucumber' then gems << "gem 'cucumber'"
|
|
68
|
+
when 'minitest' then gems << "gem 'minitest'"
|
|
69
|
+
end
|
|
70
|
+
File.write("#{dir}/Gemfile", gems.join("\n") + "\n")
|
|
71
|
+
File.write("#{dir}/config/config.yml", "browser: chrome\nurl: http://localhost:3000\n")
|
|
72
|
+
File.write("#{dir}/page_objects/abstract/page.rb", "class Page; end\n")
|
|
73
|
+
File.write("#{dir}/page_objects/abstract/component.rb", "class Component; end\n")
|
|
74
|
+
|
|
75
|
+
dir
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# ──────────────────────────────────────────────
|
|
79
|
+
# Shared examples: Page object content
|
|
80
|
+
# ──────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
shared_examples 'valid scaffolded page' do
|
|
83
|
+
it 'has frozen_string_literal' do
|
|
84
|
+
expect(page_content).to have_frozen_string_literal
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'has valid Ruby syntax' do
|
|
88
|
+
expect(page_content).to have_valid_ruby_syntax
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'inherits from Page' do
|
|
92
|
+
expect(page_content).to include('class CheckoutPage < Page')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'requires abstract page' do
|
|
96
|
+
expect(page_content).to include("require_relative '../abstract/page'")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'includes url method (config has url)' do
|
|
100
|
+
expect(page_content).to include("def url(_page)")
|
|
101
|
+
expect(page_content).to include("'checkout'")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
shared_examples 'selenium scaffolded page' do
|
|
106
|
+
it 'has driver.find_element example' do
|
|
107
|
+
expect(page_content).to include('driver.find_element')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'has private section' do
|
|
111
|
+
expect(page_content).to include('private')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'does not have capybara fill_in' do
|
|
115
|
+
expect(page_content).not_to include('fill_in')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'does not have watir browser.text_field' do
|
|
119
|
+
expect(page_content).not_to include('browser.text_field')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
shared_examples 'capybara scaffolded page' do
|
|
124
|
+
it 'has fill_in example' do
|
|
125
|
+
expect(page_content).to include('fill_in')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'has no private section' do
|
|
129
|
+
expect(page_content).not_to include('private')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'does not have selenium find_element' do
|
|
133
|
+
expect(page_content).not_to include('driver.find_element')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'does not have watir browser.text_field' do
|
|
137
|
+
expect(page_content).not_to include('browser.text_field')
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
shared_examples 'watir scaffolded page' do
|
|
142
|
+
it 'has browser.text_field example' do
|
|
143
|
+
expect(page_content).to include('browser.text_field')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'has private section' do
|
|
147
|
+
expect(page_content).to include('private')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'does not have selenium find_element' do
|
|
151
|
+
expect(page_content).not_to include('driver.find_element')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'does not have capybara fill_in' do
|
|
155
|
+
expect(page_content).not_to include('fill_in')
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ──────────────────────────────────────────────
|
|
160
|
+
# Shared examples: Spec content (RSpec)
|
|
161
|
+
# ──────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
shared_examples 'valid scaffolded spec' do
|
|
164
|
+
it 'has frozen_string_literal' do
|
|
165
|
+
expect(spec_content).to have_frozen_string_literal
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'has valid Ruby syntax' do
|
|
169
|
+
expect(spec_content).to have_valid_ruby_syntax
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'describes the page class' do
|
|
173
|
+
expect(spec_content).to include("describe 'CheckoutPage'")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'requires the page object' do
|
|
177
|
+
expect(spec_content).to include("require_relative '../page_objects/pages/checkout'")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'has pending test' do
|
|
181
|
+
expect(spec_content).to include("pending 'implement test'")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'has before block with navigation' do
|
|
185
|
+
expect(spec_content).to include('before do')
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
shared_examples 'selenium scaffolded spec' do
|
|
190
|
+
it 'instantiates page with driver' do
|
|
191
|
+
expect(spec_content).to include('CheckoutPage.new(driver)')
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'navigates with driver' do
|
|
195
|
+
expect(spec_content).to include('driver.navigate.to')
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'does not use capybara visit' do
|
|
199
|
+
expect(spec_content).not_to match(/^\s+visit '/)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
shared_examples 'capybara scaffolded spec' do
|
|
204
|
+
it 'instantiates page without arguments' do
|
|
205
|
+
expect(spec_content).to include('CheckoutPage.new')
|
|
206
|
+
expect(spec_content).not_to include('CheckoutPage.new(driver)')
|
|
207
|
+
expect(spec_content).not_to include('CheckoutPage.new(browser)')
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'uses visit for navigation' do
|
|
211
|
+
expect(spec_content).to include("visit 'checkout'")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
shared_examples 'watir scaffolded spec' do
|
|
216
|
+
it 'instantiates page with browser' do
|
|
217
|
+
expect(spec_content).to include('CheckoutPage.new(browser)')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'navigates with browser.goto' do
|
|
221
|
+
expect(spec_content).to include('browser.goto')
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# ──────────────────────────────────────────────
|
|
226
|
+
# Shared examples: Feature content (Cucumber)
|
|
227
|
+
# ──────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
shared_examples 'valid scaffolded feature' do
|
|
230
|
+
it 'has Feature keyword' do
|
|
231
|
+
expect(feature_content).to include('Feature: Checkout')
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it 'has Scenario keyword' do
|
|
235
|
+
expect(feature_content).to include('Scenario: Scenario name')
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'has Given/When/Then steps' do
|
|
239
|
+
expect(feature_content).to include('Given I am on the checkout page')
|
|
240
|
+
expect(feature_content).to include('When I perform an action')
|
|
241
|
+
expect(feature_content).to include('Then I see the expected result')
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# ──────────────────────────────────────────────
|
|
246
|
+
# Shared examples: Steps content (Cucumber)
|
|
247
|
+
# ──────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
shared_examples 'valid scaffolded steps' do
|
|
250
|
+
it 'has frozen_string_literal' do
|
|
251
|
+
expect(steps_content).to have_frozen_string_literal
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'has valid Ruby syntax' do
|
|
255
|
+
expect(steps_content).to have_valid_ruby_syntax
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'requires the page object' do
|
|
259
|
+
expect(steps_content).to include("require_relative '../../page_objects/pages/checkout'")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'has Given/When/Then blocks' do
|
|
263
|
+
expect(steps_content).to include("Given('I am on the checkout page')")
|
|
264
|
+
expect(steps_content).to include("When('I perform an action')")
|
|
265
|
+
expect(steps_content).to include("Then('I see the expected result')")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'has pending steps' do
|
|
269
|
+
expect(steps_content).to include("pending 'implement step'")
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
shared_examples 'selenium scaffolded steps' do
|
|
274
|
+
it 'instantiates page with driver' do
|
|
275
|
+
expect(steps_content).to include('CheckoutPage.new(driver)')
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it 'does not use capybara visit' do
|
|
279
|
+
expect(steps_content).not_to match(/^\s+visit '/)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
shared_examples 'capybara scaffolded steps' do
|
|
284
|
+
it 'uses visit for Given step' do
|
|
285
|
+
expect(steps_content).to include("visit 'checkout'")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it 'does not instantiate with driver' do
|
|
289
|
+
expect(steps_content).not_to include('.new(driver)')
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
shared_examples 'watir scaffolded steps' do
|
|
294
|
+
it 'instantiates page with browser' do
|
|
295
|
+
expect(steps_content).to include('CheckoutPage.new(browser)')
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# ──────────────────────────────────────────────
|
|
300
|
+
# Shared examples: Helper content
|
|
301
|
+
# ──────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
shared_examples 'valid scaffolded helper' do
|
|
304
|
+
it 'has frozen_string_literal' do
|
|
305
|
+
expect(helper_content).to have_frozen_string_literal
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'has valid Ruby syntax' do
|
|
309
|
+
expect(helper_content).to have_valid_ruby_syntax
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'defines helper module' do
|
|
313
|
+
expect(helper_content).to include('module CheckoutHelper')
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it 'has helper placeholder comment' do
|
|
317
|
+
expect(helper_content).to include('# Add your helper code here')
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# ──────────────────────────────────────────────
|
|
322
|
+
# Shared examples: Component content
|
|
323
|
+
# ──────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
shared_examples 'valid scaffolded component' do
|
|
326
|
+
it 'has frozen_string_literal' do
|
|
327
|
+
expect(component_content).to have_frozen_string_literal
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
it 'has valid Ruby syntax' do
|
|
331
|
+
expect(component_content).to have_valid_ruby_syntax
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it 'inherits from Component' do
|
|
335
|
+
expect(component_content).to include('class Sidebar < Component')
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it 'requires abstract component' do
|
|
339
|
+
expect(component_content).to include("require_relative '../abstract/component'")
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
shared_examples 'selenium scaffolded component' do
|
|
344
|
+
it 'has driver.find_element example' do
|
|
345
|
+
expect(component_content).to include('driver.find_element')
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it 'has private section' do
|
|
349
|
+
expect(component_content).to include('private')
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it 'has content method' do
|
|
353
|
+
expect(component_content).to include('def content')
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
shared_examples 'capybara scaffolded component' do
|
|
358
|
+
it 'has find(.selector) example' do
|
|
359
|
+
expect(component_content).to include("find('.selector')")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it 'has no private section' do
|
|
363
|
+
expect(component_content).not_to include('private')
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
it 'does not have driver.find_element' do
|
|
367
|
+
expect(component_content).not_to include('driver.find_element')
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
shared_examples 'watir scaffolded component' do
|
|
372
|
+
it 'has browser.element example' do
|
|
373
|
+
expect(component_content).to include('browser.element')
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
it 'has private section' do
|
|
377
|
+
expect(component_content).to include('private')
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
it 'has content method' do
|
|
381
|
+
expect(component_content).to include('def content')
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# ══════════════════════════════════════════════
|
|
386
|
+
# Test matrix: automation × framework
|
|
387
|
+
# ══════════════════════════════════════════════
|
|
388
|
+
|
|
389
|
+
WEB_AUTOMATIONS.each do |automation|
|
|
390
|
+
TEST_FRAMEWORKS.each do |framework|
|
|
391
|
+
style = scaffold_style(framework)
|
|
392
|
+
|
|
393
|
+
context "with #{framework} and #{automation}" do # rubocop:disable RSpec/ContextWording
|
|
394
|
+
let(:scaffold) { ScaffoldingCommands }
|
|
395
|
+
|
|
396
|
+
# Use absolute path so Dir.chdir is safe regardless of current CWD
|
|
397
|
+
project_dir = build_mock_project(automation, framework)
|
|
398
|
+
|
|
399
|
+
before do
|
|
400
|
+
Dir.chdir(project_dir)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
after do
|
|
404
|
+
Dir.chdir(File.dirname(project_dir))
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
after(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
|
408
|
+
FileUtils.rm_rf(project_dir)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# ── Default scaffold ───────────────────
|
|
412
|
+
|
|
413
|
+
describe 'default scaffold' do
|
|
414
|
+
before do
|
|
415
|
+
scaffold.new.invoke(:scaffold, nil, %w[checkout])
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
after do
|
|
419
|
+
FileUtils.rm_f('page_objects/pages/checkout.rb')
|
|
420
|
+
FileUtils.rm_f('spec/checkout_page_spec.rb')
|
|
421
|
+
if framework == 'cucumber'
|
|
422
|
+
FileUtils.rm_f('features/checkout.feature')
|
|
423
|
+
FileUtils.rm_f('features/step_definitions/checkout_steps.rb')
|
|
424
|
+
else
|
|
425
|
+
FileUtils.rm_rf('features')
|
|
426
|
+
FileUtils.rm_rf('spec') unless framework == 'rspec'
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it 'creates page object' do
|
|
431
|
+
expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
if style == :spec
|
|
435
|
+
it 'creates spec file' do
|
|
436
|
+
expect(Pathname.new('spec/checkout_page_spec.rb')).to be_file
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it 'does not create feature file' do
|
|
440
|
+
expect(Pathname.new('features/checkout.feature')).not_to be_file
|
|
441
|
+
end
|
|
442
|
+
else # :feature
|
|
443
|
+
it 'creates feature file' do
|
|
444
|
+
expect(Pathname.new('features/checkout.feature')).to be_file
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
it 'creates steps file' do
|
|
448
|
+
expect(Pathname.new('features/step_definitions/checkout_steps.rb')).to be_file
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Page content validation
|
|
453
|
+
let(:page_content) { File.read('page_objects/pages/checkout.rb') }
|
|
454
|
+
|
|
455
|
+
include_examples 'valid scaffolded page'
|
|
456
|
+
include_examples "#{automation} scaffolded page"
|
|
457
|
+
|
|
458
|
+
# Test content validation (depends on scaffold style)
|
|
459
|
+
if style == :spec
|
|
460
|
+
let(:spec_content) { File.read('spec/checkout_page_spec.rb') }
|
|
461
|
+
include_examples 'valid scaffolded spec'
|
|
462
|
+
include_examples "#{automation} scaffolded spec"
|
|
463
|
+
else # :feature
|
|
464
|
+
let(:feature_content) { File.read('features/checkout.feature') }
|
|
465
|
+
include_examples 'valid scaffolded feature'
|
|
466
|
+
|
|
467
|
+
let(:steps_content) { File.read('features/step_definitions/checkout_steps.rb') }
|
|
468
|
+
include_examples 'valid scaffolded steps'
|
|
469
|
+
include_examples "#{automation} scaffolded steps"
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# ── Component scaffold ─────────────────
|
|
474
|
+
|
|
475
|
+
describe 'component scaffold' do
|
|
476
|
+
before do
|
|
477
|
+
scaffold.new.invoke(:component, nil, %w[sidebar])
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
after do
|
|
481
|
+
FileUtils.rm_f('page_objects/components/sidebar.rb')
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
let(:component_content) { File.read('page_objects/components/sidebar.rb') }
|
|
485
|
+
|
|
486
|
+
include_examples 'valid scaffolded component'
|
|
487
|
+
include_examples "#{automation} scaffolded component"
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# ── Helper scaffold ────────────────────
|
|
491
|
+
|
|
492
|
+
describe 'helper scaffold' do
|
|
493
|
+
before do
|
|
494
|
+
scaffold.new.invoke(:helper, nil, %w[checkout])
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
after do
|
|
498
|
+
FileUtils.rm_f('helpers/checkout_helper.rb')
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
let(:helper_content) { File.read('helpers/checkout_helper.rb') }
|
|
502
|
+
|
|
503
|
+
include_examples 'valid scaffolded helper'
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# ── CRUD scaffold ──────────────────────
|
|
507
|
+
# CrudGenerator checks Dir.exist?('features'), Dir.exist?('test') separately.
|
|
508
|
+
|
|
509
|
+
describe 'CRUD scaffold' do
|
|
510
|
+
before do
|
|
511
|
+
scaffold.new.invoke(:scaffold, nil, %w[product --crud])
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
after do
|
|
515
|
+
%w[product_list product_create product_detail product_edit].each do |page|
|
|
516
|
+
FileUtils.rm_f("page_objects/pages/#{page}.rb")
|
|
517
|
+
FileUtils.rm_f("spec/#{page}_page_spec.rb")
|
|
518
|
+
FileUtils.rm_f("features/#{page}.feature")
|
|
519
|
+
FileUtils.rm_f("features/step_definitions/#{page}_steps.rb")
|
|
520
|
+
end
|
|
521
|
+
FileUtils.rm_rf('features') unless framework == 'cucumber'
|
|
522
|
+
FileUtils.rm_rf('spec') unless framework == 'rspec'
|
|
523
|
+
FileUtils.rm_f('models/data/product.yml')
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
it 'creates pages for all CRUD actions' do
|
|
527
|
+
%w[product_list product_create product_detail product_edit].each do |page|
|
|
528
|
+
expect(Pathname.new("page_objects/pages/#{page}.rb")).to be_file
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
it 'creates model data file' do
|
|
533
|
+
expect(Pathname.new('models/data/product.yml')).to be_file
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
it 'model data has expected sections' do
|
|
537
|
+
content = File.read('models/data/product.yml')
|
|
538
|
+
expect(content).to include('default:')
|
|
539
|
+
expect(content).to include('valid:')
|
|
540
|
+
expect(content).to include('invalid:')
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
if framework == 'cucumber'
|
|
544
|
+
it 'creates feature files for all CRUD actions' do
|
|
545
|
+
%w[product_list product_create product_detail product_edit].each do |page|
|
|
546
|
+
expect(Pathname.new("features/#{page}.feature")).to be_file
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
it 'creates step files for all CRUD actions' do
|
|
551
|
+
%w[product_list product_create product_detail product_edit].each do |page|
|
|
552
|
+
expect(Pathname.new("features/step_definitions/#{page}_steps.rb")).to be_file
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
elsif framework == 'rspec'
|
|
556
|
+
it 'creates spec files for all CRUD actions' do
|
|
557
|
+
%w[product_list product_create product_detail product_edit].each do |page|
|
|
558
|
+
expect(Pathname.new("spec/#{page}_page_spec.rb")).to be_file
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
else # minitest — CrudGenerator checks test/ and generates specs
|
|
562
|
+
it 'creates spec files for all CRUD actions' do
|
|
563
|
+
%w[product_list product_create product_detail product_edit].each do |page|
|
|
564
|
+
expect(Pathname.new("spec/#{page}_page_spec.rb")).to be_file
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
it 'all CRUD pages have valid Ruby syntax' do
|
|
570
|
+
%w[product_list product_create product_detail product_edit].each do |page|
|
|
571
|
+
content = File.read("page_objects/pages/#{page}.rb")
|
|
572
|
+
expect(content).to have_valid_ruby_syntax
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
it 'all CRUD pages have frozen_string_literal' do
|
|
577
|
+
%w[product_list product_create product_detail product_edit].each do |page|
|
|
578
|
+
content = File.read("page_objects/pages/#{page}.rb")
|
|
579
|
+
expect(content).to have_frozen_string_literal
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# ── Selective --with ───────────────────
|
|
585
|
+
|
|
586
|
+
describe 'selective scaffold' do
|
|
587
|
+
after do
|
|
588
|
+
%w[settings payment order].each do |name|
|
|
589
|
+
FileUtils.rm_f("page_objects/pages/#{name}.rb")
|
|
590
|
+
FileUtils.rm_f("spec/#{name}_page_spec.rb")
|
|
591
|
+
FileUtils.rm_f("helpers/#{name}_helper.rb")
|
|
592
|
+
FileUtils.rm_f("models/data/#{name}.yml")
|
|
593
|
+
end
|
|
594
|
+
FileUtils.rm_rf('features') unless framework == 'cucumber'
|
|
595
|
+
FileUtils.rm_rf('spec') unless framework == 'rspec'
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
it 'generates only page when --with page' do
|
|
599
|
+
scaffold.new.invoke(:scaffold, nil, %w[settings --with page])
|
|
600
|
+
expect(Pathname.new('page_objects/pages/settings.rb')).to be_file
|
|
601
|
+
expect(Pathname.new('spec/settings_page_spec.rb')).not_to be_file
|
|
602
|
+
expect(Pathname.new('features/settings.feature')).not_to be_file
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
it 'generates page and helper with --with page helper' do
|
|
606
|
+
scaffold.new.invoke(:scaffold, nil, %w[payment --with page helper])
|
|
607
|
+
expect(Pathname.new('page_objects/pages/payment.rb')).to be_file
|
|
608
|
+
expect(Pathname.new('helpers/payment_helper.rb')).to be_file
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
it 'generates model data with --with model' do
|
|
612
|
+
scaffold.new.invoke(:scaffold, nil, %w[order --with model])
|
|
613
|
+
expect(Pathname.new('models/data/order.yml')).to be_file
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# ── Destroy ────────────────────────────
|
|
618
|
+
|
|
619
|
+
describe 'destroy' do
|
|
620
|
+
it 'removes scaffolded files' do
|
|
621
|
+
scaffold.new.invoke(:scaffold, nil, %w[temp_item])
|
|
622
|
+
expect(Pathname.new('page_objects/pages/temp_item.rb')).to be_file
|
|
623
|
+
|
|
624
|
+
scaffold.new.invoke(:destroy, nil, %w[temp_item])
|
|
625
|
+
expect(Pathname.new('page_objects/pages/temp_item.rb')).not_to be_file
|
|
626
|
+
|
|
627
|
+
if style == :spec
|
|
628
|
+
expect(Pathname.new('spec/temp_item_page_spec.rb')).not_to be_file
|
|
629
|
+
else
|
|
630
|
+
expect(Pathname.new('features/temp_item.feature')).not_to be_file
|
|
631
|
+
expect(Pathname.new('features/step_definitions/temp_item_steps.rb')).not_to be_file
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# ── Batch scaffold ─────────────────────
|
|
637
|
+
|
|
638
|
+
describe 'batch scaffold' do
|
|
639
|
+
after do
|
|
640
|
+
%w[search filter].each do |name|
|
|
641
|
+
FileUtils.rm_f("page_objects/pages/#{name}.rb")
|
|
642
|
+
FileUtils.rm_f("spec/#{name}_page_spec.rb")
|
|
643
|
+
FileUtils.rm_f("features/#{name}.feature")
|
|
644
|
+
FileUtils.rm_f("features/step_definitions/#{name}_steps.rb")
|
|
645
|
+
end
|
|
646
|
+
FileUtils.rm_rf('features') unless framework == 'cucumber'
|
|
647
|
+
FileUtils.rm_rf('spec') unless framework == 'rspec'
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
it 'generates multiple scaffolds at once' do
|
|
651
|
+
scaffold.new.invoke(:scaffold, nil, %w[search filter])
|
|
652
|
+
expect(Pathname.new('page_objects/pages/search.rb')).to be_file
|
|
653
|
+
expect(Pathname.new('page_objects/pages/filter.rb')).to be_file
|
|
654
|
+
|
|
655
|
+
if style == :spec
|
|
656
|
+
expect(Pathname.new('spec/search_page_spec.rb')).to be_file
|
|
657
|
+
expect(Pathname.new('spec/filter_page_spec.rb')).to be_file
|
|
658
|
+
else
|
|
659
|
+
expect(Pathname.new('features/search.feature')).to be_file
|
|
660
|
+
expect(Pathname.new('features/filter.feature')).to be_file
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# ── Name normalization ─────────────────
|
|
666
|
+
|
|
667
|
+
describe 'name normalization' do
|
|
668
|
+
after do
|
|
669
|
+
%w[cart shopping_cart checkout].each do |name|
|
|
670
|
+
FileUtils.rm_f("page_objects/pages/#{name}.rb")
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
it 'strips _page suffix' do
|
|
675
|
+
scaffold.new.invoke(:page, nil, %w[cart_page])
|
|
676
|
+
expect(Pathname.new('page_objects/pages/cart.rb')).to be_file
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
it 'converts CamelCase to snake_case' do
|
|
680
|
+
scaffold.new.invoke(:page, nil, %w[ShoppingCart])
|
|
681
|
+
expect(Pathname.new('page_objects/pages/shopping_cart.rb')).to be_file
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
it 'handles CamelCase with Page suffix' do
|
|
685
|
+
scaffold.new.invoke(:page, nil, %w[CheckoutPage])
|
|
686
|
+
expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# ── Relationships ──────────────────────
|
|
691
|
+
|
|
692
|
+
describe 'relationships' do
|
|
693
|
+
before do
|
|
694
|
+
File.write('page_objects/pages/login.rb', "class LoginPage < Page; end\n")
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
after do
|
|
698
|
+
FileUtils.rm_f('page_objects/pages/dashboard.rb')
|
|
699
|
+
FileUtils.rm_f('spec/dashboard_page_spec.rb')
|
|
700
|
+
FileUtils.rm_f('features/dashboard.feature')
|
|
701
|
+
FileUtils.rm_f('features/step_definitions/dashboard_steps.rb')
|
|
702
|
+
FileUtils.rm_f('page_objects/pages/login.rb')
|
|
703
|
+
FileUtils.rm_rf('features') unless framework == 'cucumber'
|
|
704
|
+
FileUtils.rm_rf('spec') unless framework == 'rspec'
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
it 'adds require_relative for dependent page' do
|
|
708
|
+
scaffold.new.invoke(:page, nil, %w[dashboard --uses login])
|
|
709
|
+
content = File.read('page_objects/pages/dashboard.rb')
|
|
710
|
+
expect(content).to include("require_relative 'login'")
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
if style == :spec
|
|
714
|
+
it 'adds dependency to spec let declarations' do
|
|
715
|
+
scaffold.new.invoke(:spec, nil, %w[dashboard --uses login])
|
|
716
|
+
content = File.read('spec/dashboard_page_spec.rb')
|
|
717
|
+
expect(content).to include('LoginPage')
|
|
718
|
+
|
|
719
|
+
case automation
|
|
720
|
+
when 'selenium' then expect(content).to include('LoginPage.new(driver)')
|
|
721
|
+
when 'watir' then expect(content).to include('LoginPage.new(browser)')
|
|
722
|
+
when 'capybara' then expect(content).to include('LoginPage.new')
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# ── Template overrides ─────────────────
|
|
729
|
+
|
|
730
|
+
describe 'template overrides' do
|
|
731
|
+
after do
|
|
732
|
+
FileUtils.rm_rf('.ruby_raider')
|
|
733
|
+
FileUtils.rm_f('page_objects/pages/custom_override.rb')
|
|
734
|
+
FileUtils.rm_f('page_objects/pages/default_fallback.rb')
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
it 'uses override template when present' do
|
|
738
|
+
FileUtils.mkdir_p('.ruby_raider/templates')
|
|
739
|
+
File.write('.ruby_raider/templates/page_object.tt', <<~ERB)
|
|
740
|
+
# frozen_string_literal: true
|
|
741
|
+
|
|
742
|
+
class <%= page_class_name %> < Page
|
|
743
|
+
# E2E_CUSTOM_MARKER
|
|
744
|
+
end
|
|
745
|
+
ERB
|
|
746
|
+
|
|
747
|
+
scaffold.new.invoke(:page, nil, %w[custom_override])
|
|
748
|
+
content = File.read('page_objects/pages/custom_override.rb')
|
|
749
|
+
expect(content).to include('E2E_CUSTOM_MARKER')
|
|
750
|
+
expect(content).to include('class CustomOverridePage < Page')
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
it 'falls back to default template without override' do
|
|
754
|
+
scaffold.new.invoke(:page, nil, %w[default_fallback])
|
|
755
|
+
content = File.read('page_objects/pages/default_fallback.rb')
|
|
756
|
+
expect(content).not_to include('E2E_CUSTOM_MARKER')
|
|
757
|
+
expect(content).to include('class DefaultFallbackPage < Page')
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# ── Dry run ────────────────────────────
|
|
762
|
+
|
|
763
|
+
describe 'dry run' do
|
|
764
|
+
it 'does not create files with --dry-run' do
|
|
765
|
+
expect do
|
|
766
|
+
scaffold.new.invoke(:page, nil, %w[phantom --dry-run])
|
|
767
|
+
end.to output(/phantom/).to_stdout
|
|
768
|
+
|
|
769
|
+
expect(Pathname.new('page_objects/pages/phantom.rb')).not_to be_file
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
end
|
|
@@ -87,6 +87,44 @@ RSpec.describe ScaffoldProjectDetector do
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
describe '.validate_project' do
|
|
91
|
+
it 'warns when Gemfile is missing' do
|
|
92
|
+
warnings = described_class.validate_project
|
|
93
|
+
expect(warnings).to include(match(/Gemfile not found/))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'warns when config/config.yml is missing' do
|
|
97
|
+
File.write('Gemfile', "gem 'rspec'\n")
|
|
98
|
+
warnings = described_class.validate_project
|
|
99
|
+
expect(warnings).to include(match(/config\.yml not found/))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'returns no warnings for valid project' do
|
|
103
|
+
File.write('Gemfile', "gem 'rspec'\n")
|
|
104
|
+
FileUtils.mkdir_p('config')
|
|
105
|
+
File.write('config/config.yml', "browser: chrome\n")
|
|
106
|
+
expect(described_class.validate_project).to be_empty
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe '.config' do
|
|
111
|
+
it 'returns empty hash when config missing' do
|
|
112
|
+
expect(described_class.config).to eq({})
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'parses valid config' do
|
|
116
|
+
FileUtils.mkdir_p('config')
|
|
117
|
+
File.write('config/config.yml', "browser: chrome\nurl: http://localhost\n")
|
|
118
|
+
expect(described_class.config).to eq({ 'browser' => 'chrome', 'url' => 'http://localhost' })
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'raises Error on malformed YAML' do
|
|
122
|
+
FileUtils.mkdir_p('config')
|
|
123
|
+
File.write('config/config.yml', "browser: chrome\n bad:\nindent")
|
|
124
|
+
expect { described_class.config }.to raise_error(ScaffoldProjectDetector::Error, /invalid YAML syntax/)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
90
128
|
describe '.detect' do
|
|
91
129
|
before do
|
|
92
130
|
File.write('Gemfile', "gem 'selenium-webdriver'\ngem 'rspec'\n")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'erb'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'Headless config support in driver templates' do
|
|
8
|
+
let(:tmp_dir) { 'tmp_headless_test' }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
FileUtils.mkdir_p(tmp_dir)
|
|
12
|
+
Dir.chdir(tmp_dir)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
after do
|
|
16
|
+
Dir.chdir('..')
|
|
17
|
+
FileUtils.rm_rf(tmp_dir)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe 'selenium driver template' do
|
|
21
|
+
it 'checks config headless key' do
|
|
22
|
+
content = File.read(File.expand_path('../../lib/generators/templates/helpers/partials/selenium_driver.tt',
|
|
23
|
+
__dir__))
|
|
24
|
+
expect(content).to include("@config['headless']")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'checks ENV HEADLESS' do
|
|
28
|
+
content = File.read(File.expand_path('../../lib/generators/templates/helpers/partials/selenium_driver.tt',
|
|
29
|
+
__dir__))
|
|
30
|
+
expect(content).to include("ENV['HEADLESS']")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe 'capybara helper template' do
|
|
35
|
+
it 'checks config headless key' do
|
|
36
|
+
content = File.read(File.expand_path('../../lib/generators/templates/helpers/capybara_helper.tt', __dir__))
|
|
37
|
+
expect(content).to include("config['headless']")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe 'browser helper template (watir)' do
|
|
42
|
+
it 'checks config headless key' do
|
|
43
|
+
content = File.read(File.expand_path('../../lib/generators/templates/helpers/browser_helper.tt', __dir__))
|
|
44
|
+
expect(content).to include("config['headless']")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe 'headless via config.yml' do
|
|
49
|
+
it 'config key is used when YAML has headless: true' do
|
|
50
|
+
FileUtils.mkdir_p('config')
|
|
51
|
+
File.write('config/config.yml', <<~YAML)
|
|
52
|
+
browser: chrome
|
|
53
|
+
headless: true
|
|
54
|
+
browser_arguments:
|
|
55
|
+
chrome:
|
|
56
|
+
- no-sandbox
|
|
57
|
+
YAML
|
|
58
|
+
|
|
59
|
+
config = YAML.load_file('config/config.yml')
|
|
60
|
+
args = config['browser_arguments'][config['browser']]
|
|
61
|
+
|
|
62
|
+
# Simulate the template logic
|
|
63
|
+
args += ['--headless'] if (ENV['HEADLESS'] || config['headless']) && !args.include?('--headless')
|
|
64
|
+
|
|
65
|
+
expect(args).to include('--headless')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'config key is not used when headless: false' do
|
|
69
|
+
FileUtils.mkdir_p('config')
|
|
70
|
+
File.write('config/config.yml', <<~YAML)
|
|
71
|
+
browser: chrome
|
|
72
|
+
headless: false
|
|
73
|
+
browser_arguments:
|
|
74
|
+
chrome:
|
|
75
|
+
- no-sandbox
|
|
76
|
+
YAML
|
|
77
|
+
|
|
78
|
+
config = YAML.load_file('config/config.yml')
|
|
79
|
+
args = config['browser_arguments'][config['browser']]
|
|
80
|
+
|
|
81
|
+
# Temporarily clear ENV to isolate config behavior
|
|
82
|
+
original_env = ENV.delete('HEADLESS')
|
|
83
|
+
args += ['--headless'] if (ENV['HEADLESS'] || config['headless']) && !args.include?('--headless')
|
|
84
|
+
ENV['HEADLESS'] = original_env if original_env
|
|
85
|
+
|
|
86
|
+
expect(args).not_to include('--headless')
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require_relative '../../lib/utilities/utilities'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Utilities do
|
|
8
|
+
let(:tmp_dir) { 'tmp_utilities_test' }
|
|
9
|
+
let(:config_path) { 'config/config.yml' }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
FileUtils.mkdir_p(tmp_dir)
|
|
13
|
+
Dir.chdir(tmp_dir)
|
|
14
|
+
FileUtils.mkdir_p('config')
|
|
15
|
+
File.write(config_path, <<~YAML)
|
|
16
|
+
browser: chrome
|
|
17
|
+
url: 'http://localhost:3000'
|
|
18
|
+
timeout: 10
|
|
19
|
+
viewport:
|
|
20
|
+
width: 1920
|
|
21
|
+
height: 1080
|
|
22
|
+
browser_arguments:
|
|
23
|
+
chrome:
|
|
24
|
+
- no-sandbox
|
|
25
|
+
- disable-dev-shm-usage
|
|
26
|
+
firefox:
|
|
27
|
+
- acceptInsecureCerts
|
|
28
|
+
YAML
|
|
29
|
+
# Reset cached config between tests
|
|
30
|
+
described_class.instance_variable_set(:@config, nil)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
after do
|
|
34
|
+
Dir.chdir('..')
|
|
35
|
+
FileUtils.rm_rf(tmp_dir)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '.browser_options=' do
|
|
39
|
+
it 'writes to browser_arguments for the current browser' do
|
|
40
|
+
described_class.browser_options = %w[no-sandbox headless]
|
|
41
|
+
config = YAML.load_file(config_path)
|
|
42
|
+
expect(config['browser_arguments']['chrome']).to eq(%w[no-sandbox headless])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'writes to the correct browser key when browser is firefox' do
|
|
46
|
+
described_class.browser = 'firefox'
|
|
47
|
+
described_class.instance_variable_set(:@config, nil)
|
|
48
|
+
described_class.browser_options = %w[headless]
|
|
49
|
+
config = YAML.load_file(config_path)
|
|
50
|
+
expect(config['browser_arguments']['firefox']).to eq(%w[headless])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'preserves other browser arguments' do
|
|
54
|
+
described_class.browser_options = %w[headless]
|
|
55
|
+
config = YAML.load_file(config_path)
|
|
56
|
+
expect(config['browser_arguments']['firefox']).to eq(%w[acceptInsecureCerts])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '.delete_browser_options' do
|
|
61
|
+
it 'removes browser_arguments for the current browser' do
|
|
62
|
+
described_class.delete_browser_options
|
|
63
|
+
config = YAML.load_file(config_path)
|
|
64
|
+
expect(config['browser_arguments']['chrome']).to be_nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'preserves other browser arguments' do
|
|
68
|
+
described_class.delete_browser_options
|
|
69
|
+
config = YAML.load_file(config_path)
|
|
70
|
+
expect(config['browser_arguments']['firefox']).to eq(%w[acceptInsecureCerts])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '.headless=' do
|
|
75
|
+
it 'sets headless to true' do
|
|
76
|
+
described_class.headless = true
|
|
77
|
+
config = YAML.load_file(config_path)
|
|
78
|
+
expect(config['headless']).to be true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'sets headless to false' do
|
|
82
|
+
described_class.headless = true
|
|
83
|
+
described_class.instance_variable_set(:@config, nil)
|
|
84
|
+
described_class.headless = false
|
|
85
|
+
config = YAML.load_file(config_path)
|
|
86
|
+
expect(config['headless']).to be false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '.viewport=' do
|
|
91
|
+
it 'sets viewport dimensions' do
|
|
92
|
+
described_class.viewport = '375x812'
|
|
93
|
+
config = YAML.load_file(config_path)
|
|
94
|
+
expect(config['viewport']).to eq({ 'width' => 375, 'height' => 812 })
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe '.browser_options= writes to browser_arguments not browser_options' do
|
|
99
|
+
it 'does not create a browser_options key' do
|
|
100
|
+
described_class.browser_options = %w[headless]
|
|
101
|
+
config = YAML.load_file(config_path)
|
|
102
|
+
expect(config).not_to have_key('browser_options')
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_raider
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Agustin Pequeno
|
|
@@ -375,6 +375,7 @@ files:
|
|
|
375
375
|
- spec/adopter/project_analyzer_spec.rb
|
|
376
376
|
- spec/adopter/project_detector_spec.rb
|
|
377
377
|
- spec/commands/raider_commands_spec.rb
|
|
378
|
+
- spec/commands/scaffolding_commands_spec.rb
|
|
378
379
|
- spec/generators/fixtures/templates/test.tt
|
|
379
380
|
- spec/generators/fixtures/templates/test_partial.tt
|
|
380
381
|
- spec/generators/generator_spec.rb
|
|
@@ -409,6 +410,7 @@ files:
|
|
|
409
410
|
- spec/integration/generators/rspec_generator_spec.rb
|
|
410
411
|
- spec/integration/generators/skip_flags_spec.rb
|
|
411
412
|
- spec/integration/generators/visual_addon_spec.rb
|
|
413
|
+
- spec/integration/scaffolding_e2e_spec.rb
|
|
412
414
|
- spec/integration/settings_helper.rb
|
|
413
415
|
- spec/integration/spec_helper.rb
|
|
414
416
|
- spec/llm/client_spec.rb
|
|
@@ -429,6 +431,8 @@ files:
|
|
|
429
431
|
- spec/system/support/system_test_helper.rb
|
|
430
432
|
- spec/system/watir_spec.rb
|
|
431
433
|
- spec/utilities/desktop_downloader_spec.rb
|
|
434
|
+
- spec/utilities/headless_config_spec.rb
|
|
435
|
+
- spec/utilities/utilities_spec.rb
|
|
432
436
|
homepage: https://github.com/RubyRaider/ruby_raider
|
|
433
437
|
licenses:
|
|
434
438
|
- MIT
|