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