ruby_raider 1.1.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/integration.yml +4 -6
- data/.github/workflows/reek.yml +6 -5
- data/.github/workflows/release.yml +175 -0
- data/.github/workflows/rubocop.yml +7 -6
- data/.github/workflows/system_tests.yml +83 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +24 -0
- data/README.md +3 -1
- data/RELEASE.md +412 -0
- data/RELEASE_QUICK_GUIDE.md +77 -0
- data/bin/release +186 -0
- data/lib/adopter/adopt_menu.rb +150 -0
- data/lib/adopter/converters/base_converter.rb +85 -0
- data/lib/adopter/converters/identity_converter.rb +56 -0
- data/lib/adopter/migration_plan.rb +75 -0
- data/lib/adopter/migrator.rb +96 -0
- data/lib/adopter/plan_builder.rb +278 -0
- data/lib/adopter/project_analyzer.rb +256 -0
- data/lib/adopter/project_detector.rb +159 -0
- data/lib/commands/adopt_commands.rb +43 -0
- data/lib/generators/automation/templates/account.tt +9 -5
- data/lib/generators/automation/templates/appium_caps.tt +60 -6
- data/lib/generators/automation/templates/home.tt +4 -4
- data/lib/generators/automation/templates/login.tt +61 -4
- data/lib/generators/automation/templates/page.tt +13 -7
- data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -4
- data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -1
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -4
- data/lib/generators/automation/templates/partials/visit_method.tt +11 -1
- data/lib/generators/automation/templates/pdp.tt +1 -1
- data/lib/generators/cucumber/templates/env.tt +6 -4
- data/lib/generators/cucumber/templates/partials/capybara_env.tt +20 -0
- data/lib/generators/cucumber/templates/partials/capybara_world.tt +6 -0
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +2 -2
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -3
- data/lib/generators/cucumber/templates/steps.tt +2 -2
- data/lib/generators/cucumber/templates/world.tt +5 -3
- data/lib/generators/generator.rb +14 -2
- data/lib/generators/helper_generator.rb +16 -3
- data/lib/generators/infrastructure/github_generator.rb +6 -0
- data/lib/generators/infrastructure/templates/github.tt +11 -7
- data/lib/generators/infrastructure/templates/github_appium.tt +108 -0
- data/lib/generators/infrastructure/templates/gitlab.tt +5 -2
- data/lib/generators/invoke_generators.rb +1 -0
- data/lib/generators/menu_generator.rb +2 -0
- data/lib/generators/minitest/minitest_generator.rb +23 -0
- data/lib/generators/minitest/templates/test.tt +93 -0
- data/lib/generators/rspec/templates/spec.tt +12 -10
- data/lib/generators/template_renderer/partial_cache.rb +116 -0
- data/lib/generators/template_renderer/partial_resolver.rb +103 -0
- data/lib/generators/template_renderer/template_error.rb +50 -0
- data/lib/generators/template_renderer.rb +90 -0
- data/lib/generators/templates/common/config.tt +2 -2
- data/lib/generators/templates/common/gemfile.tt +15 -3
- data/lib/generators/templates/common/partials/web_config.tt +1 -1
- data/lib/generators/templates/common/read_me.tt +3 -1
- data/lib/generators/templates/helpers/allure_helper.tt +2 -2
- data/lib/generators/templates/helpers/browser_helper.tt +1 -0
- data/lib/generators/templates/helpers/capybara_helper.tt +28 -0
- data/lib/generators/templates/helpers/driver_helper.tt +1 -1
- data/lib/generators/templates/helpers/partials/allure_imports.tt +3 -1
- data/lib/generators/templates/helpers/partials/allure_requirements.tt +3 -1
- data/lib/generators/templates/helpers/partials/appium_driver.tt +46 -0
- data/lib/generators/templates/helpers/partials/axe_driver.tt +10 -0
- data/lib/generators/templates/helpers/partials/browserstack_config.tt +13 -0
- data/lib/generators/templates/helpers/partials/driver_and_options.tt +6 -114
- data/lib/generators/templates/helpers/partials/quit_driver.tt +3 -1
- data/lib/generators/templates/helpers/partials/screenshot.tt +3 -1
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +25 -0
- data/lib/generators/templates/helpers/spec_helper.tt +17 -4
- data/lib/generators/templates/helpers/test_helper.tt +26 -0
- data/lib/generators/templates/helpers/visual_spec_helper.tt +1 -1
- data/lib/ruby_raider.rb +5 -0
- data/lib/version +1 -1
- data/spec/adopter/adopt_menu_spec.rb +176 -0
- data/spec/adopter/converters/identity_converter_spec.rb +145 -0
- data/spec/adopter/migration_plan_spec.rb +113 -0
- data/spec/adopter/migrator_spec.rb +277 -0
- data/spec/adopter/plan_builder_spec.rb +298 -0
- data/spec/adopter/project_analyzer_spec.rb +337 -0
- data/spec/adopter/project_detector_spec.rb +295 -0
- data/spec/generators/fixtures/templates/test.tt +1 -0
- data/spec/generators/fixtures/templates/test_partial.tt +1 -0
- data/spec/generators/template_renderer_spec.rb +298 -0
- data/spec/integration/commands/scaffolding_commands_spec.rb +2 -2
- data/spec/integration/commands/utility_commands_spec.rb +2 -2
- data/spec/integration/end_to_end_spec.rb +325 -0
- data/spec/integration/generators/automation_generator_spec.rb +11 -11
- data/spec/integration/generators/common_generator_spec.rb +40 -40
- data/spec/integration/generators/cucumber_generator_spec.rb +7 -7
- data/spec/integration/generators/github_generator_spec.rb +8 -8
- data/spec/integration/generators/gitlab_generator_spec.rb +8 -8
- data/spec/integration/generators/helpers_generator_spec.rb +73 -35
- data/spec/integration/generators/minitest_generator_spec.rb +70 -0
- data/spec/integration/generators/rspec_generator_spec.rb +7 -7
- data/spec/integration/settings_helper.rb +1 -1
- data/spec/integration/spec_helper.rb +20 -2
- data/spec/system/capybara_spec.rb +42 -0
- data/spec/system/selenium_spec.rb +19 -17
- data/spec/system/support/system_test_helper.rb +35 -0
- data/spec/system/watir_spec.rb +19 -17
- metadata +46 -16
- data/.github/workflows/push_gem.yml +0 -37
- data/.github/workflows/selenium.yml +0 -22
- data/.github/workflows/watir.yml +0 -22
- data/lib/generators/automation/templates/partials/android_caps.tt +0 -17
- data/lib/generators/automation/templates/partials/cross_platform_caps.tt +0 -25
- data/lib/generators/automation/templates/partials/ios_caps.tt +0 -18
- data/lib/generators/automation/templates/partials/selenium_account.tt +0 -9
- data/lib/generators/automation/templates/partials/selenium_login.tt +0 -34
- data/lib/generators/automation/templates/partials/watir_account.tt +0 -7
- data/lib/generators/automation/templates/partials/watir_login.tt +0 -32
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
name: Appium Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
<%- if ios? || cross_platform? -%>
|
|
11
|
+
ios-tests:
|
|
12
|
+
runs-on: macos-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: ruby/setup-ruby@v1
|
|
16
|
+
with:
|
|
17
|
+
ruby-version: 3.4.0
|
|
18
|
+
bundler-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Download iOS app
|
|
21
|
+
uses: dawidd6/action-download-artifact@v6
|
|
22
|
+
with:
|
|
23
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
24
|
+
repo: RaiderHQ/raider_test_app
|
|
25
|
+
workflow: build-ios.yml
|
|
26
|
+
name: raider-test-app-ios
|
|
27
|
+
path: apps/
|
|
28
|
+
|
|
29
|
+
- name: Unzip iOS app
|
|
30
|
+
run: |
|
|
31
|
+
cd apps
|
|
32
|
+
unzip RaiderTestApp.app.zip
|
|
33
|
+
|
|
34
|
+
- name: Start iOS Simulator
|
|
35
|
+
uses: futureware-tech/simulator-action@v3
|
|
36
|
+
with:
|
|
37
|
+
model: 'iPhone 15'
|
|
38
|
+
os_version: '17'
|
|
39
|
+
|
|
40
|
+
- name: Install and start Appium
|
|
41
|
+
run: |
|
|
42
|
+
npm install -g appium
|
|
43
|
+
appium driver install xcuitest
|
|
44
|
+
appium &
|
|
45
|
+
sleep 5
|
|
46
|
+
|
|
47
|
+
- name: Run tests
|
|
48
|
+
<%- if cucumber? -%>
|
|
49
|
+
run: bundle exec cucumber features/ --format pretty
|
|
50
|
+
<%- elsif minitest? -%>
|
|
51
|
+
run: bundle exec ruby -Itest test/test_pdp_page.rb
|
|
52
|
+
<%- else -%>
|
|
53
|
+
run: bundle exec rspec spec/ --format documentation
|
|
54
|
+
<%- end -%>
|
|
55
|
+
env:
|
|
56
|
+
APP_PATH: ${{ github.workspace }}/apps/RaiderTestApp.app
|
|
57
|
+
<%- end -%>
|
|
58
|
+
|
|
59
|
+
<%- if android? || cross_platform? -%>
|
|
60
|
+
android-tests:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- uses: ruby/setup-ruby@v1
|
|
65
|
+
with:
|
|
66
|
+
ruby-version: 3.4.0
|
|
67
|
+
bundler-cache: true
|
|
68
|
+
|
|
69
|
+
- name: Download Android app
|
|
70
|
+
uses: dawidd6/action-download-artifact@v6
|
|
71
|
+
with:
|
|
72
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
73
|
+
repo: RaiderHQ/raider_test_app
|
|
74
|
+
workflow: build-android.yml
|
|
75
|
+
name: raider-test-app-android
|
|
76
|
+
path: apps/
|
|
77
|
+
|
|
78
|
+
- name: Set up JDK 17
|
|
79
|
+
uses: actions/setup-java@v4
|
|
80
|
+
with:
|
|
81
|
+
java-version: '17'
|
|
82
|
+
distribution: 'temurin'
|
|
83
|
+
|
|
84
|
+
- name: Install and start Appium
|
|
85
|
+
run: |
|
|
86
|
+
npm install -g appium
|
|
87
|
+
appium driver install uiautomator2
|
|
88
|
+
appium &
|
|
89
|
+
sleep 5
|
|
90
|
+
|
|
91
|
+
- name: Run Android Emulator and Tests
|
|
92
|
+
uses: reactivecircus/android-emulator-runner@v2
|
|
93
|
+
with:
|
|
94
|
+
api-level: 34
|
|
95
|
+
target: google_apis
|
|
96
|
+
arch: x86_64
|
|
97
|
+
profile: pixel_8
|
|
98
|
+
script: |
|
|
99
|
+
<%- if cucumber? -%>
|
|
100
|
+
bundle exec cucumber features/ --format pretty
|
|
101
|
+
<%- elsif minitest? -%>
|
|
102
|
+
bundle exec ruby -Itest test/test_pdp_page.rb
|
|
103
|
+
<%- else -%>
|
|
104
|
+
bundle exec rspec spec/ --format documentation
|
|
105
|
+
<%- end -%>
|
|
106
|
+
env:
|
|
107
|
+
APP_PATH: ${{ github.workspace }}/apps/app-release.apk
|
|
108
|
+
<%- end -%>
|
|
@@ -5,6 +5,7 @@ stages:
|
|
|
5
5
|
|
|
6
6
|
variables:
|
|
7
7
|
RUBY_VERSION: "3.4.0"
|
|
8
|
+
HEADLESS: "true"
|
|
8
9
|
|
|
9
10
|
setup_ruby:
|
|
10
11
|
stage: setup
|
|
@@ -18,9 +19,11 @@ setup_ruby:
|
|
|
18
19
|
run_tests:
|
|
19
20
|
stage: test
|
|
20
21
|
image: ruby:${RUBY_VERSION}
|
|
21
|
-
|
|
22
|
+
before_script:
|
|
23
|
+
- apt-get update -qq && apt-get install -y -qq chromium chromium-driver
|
|
22
24
|
- mkdir -p allure-results
|
|
23
|
-
|
|
25
|
+
script:
|
|
26
|
+
- <%- if framework == 'cucumber' -%>bundle exec cucumber features --format pretty<%- elsif minitest? -%>bundle exec ruby -Itest test/test_login_page.rb<%- else -%>bundle exec rspec spec --format documentation<%- end %>
|
|
24
27
|
artifacts:
|
|
25
28
|
paths:
|
|
26
29
|
- allure-results/
|
|
@@ -4,6 +4,7 @@ require_relative 'automation/automation_generator'
|
|
|
4
4
|
require_relative 'common_generator'
|
|
5
5
|
require_relative 'cucumber/cucumber_generator'
|
|
6
6
|
require_relative 'helper_generator'
|
|
7
|
+
require_relative 'minitest/minitest_generator'
|
|
7
8
|
require_relative 'rspec/rspec_generator'
|
|
8
9
|
|
|
9
10
|
# :reek:FeatureEnvy { enabled: false }
|
|
@@ -53,6 +53,7 @@ class MenuGenerator
|
|
|
53
53
|
prompt.select('Please select your test framework') do |menu|
|
|
54
54
|
menu.choice :Cucumber, -> { select_ci_platform('Cucumber', automation) }
|
|
55
55
|
menu.choice :Rspec, -> { select_ci_platform('Rspec', automation) }
|
|
56
|
+
menu.choice :Minitest, -> { select_ci_platform('Minitest', automation) }
|
|
56
57
|
menu.choice :Quit, -> { exit }
|
|
57
58
|
end
|
|
58
59
|
end
|
|
@@ -92,6 +93,7 @@ class MenuGenerator
|
|
|
92
93
|
|
|
93
94
|
def automation_options(menu)
|
|
94
95
|
menu.choice :Selenium, -> { choose_test_framework('selenium') }
|
|
96
|
+
menu.choice :Capybara, -> { choose_test_framework('capybara') }
|
|
95
97
|
menu.choice :Appium, -> { choose_test_framework('appium') }
|
|
96
98
|
menu.choice :Watir, -> { choose_test_framework('watir') }
|
|
97
99
|
menu.choice :Applitools, -> { choose_test_framework('applitools') }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../generator'
|
|
4
|
+
|
|
5
|
+
class MinitestGenerator < Generator
|
|
6
|
+
def generate_login_test
|
|
7
|
+
return unless web? && !visual?
|
|
8
|
+
|
|
9
|
+
template('test.tt', "#{name}/test/test_login_page.rb")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def generate_pdp_test
|
|
13
|
+
return unless mobile?
|
|
14
|
+
|
|
15
|
+
template('test.tt', "#{name}/test/test_pdp_page.rb")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate_account_test
|
|
19
|
+
return unless visual?
|
|
20
|
+
|
|
21
|
+
template('test.tt', "#{name}/test/test_account_page.rb")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<%- if selenium_based? || watir? || capybara? -%>
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../helpers/test_helper'
|
|
5
|
+
require_relative '../models/model_factory'
|
|
6
|
+
require_relative '../page_objects/pages/account'
|
|
7
|
+
require_relative '../page_objects/pages/login'
|
|
8
|
+
|
|
9
|
+
<%- if axe? %>
|
|
10
|
+
class TestLogin < Minitest::Test
|
|
11
|
+
def setup
|
|
12
|
+
@login = Login.new(driver)
|
|
13
|
+
@account = Account.new(driver)
|
|
14
|
+
@user = ModelFactory.for('users')['registered user']
|
|
15
|
+
@login.visit
|
|
16
|
+
@login.log_as(@user['username'], @user['password'])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def teardown
|
|
20
|
+
driver.quit
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_no_accessibility_errors_on_page
|
|
24
|
+
assert_axe_clean @account.page
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_no_accessibility_errors_on_transaction_history
|
|
28
|
+
assert_axe_clean @account.page, within: '.account-card'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_no_accessibility_errors_on_heading
|
|
32
|
+
assert_axe_clean @account.page, within: '.page-title'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
<%- elsif selenium_based? || watir? || capybara? -%>
|
|
36
|
+
class TestLogin < Minitest::Test
|
|
37
|
+
def setup
|
|
38
|
+
@user = ModelFactory.for('users')['registered user']
|
|
39
|
+
@login_page = <% if capybara? -%>Login.new<% elsif watir? -%>Login.new(browser)<% else -%>Login.new(driver)<% end %>
|
|
40
|
+
@account_page = <% if capybara? -%>Account.new<% elsif watir? -%>Account.new(browser)<% else -%>Account.new(driver)<% end %>
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def teardown
|
|
44
|
+
<%= partial('quit_driver', strip: true) %>
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_login_with_right_credentials
|
|
48
|
+
@login_page.<% if capybara? -%>visit_page<% else -%>visit<% end %>
|
|
49
|
+
@login_page.login(@user['username'], @user['password'])
|
|
50
|
+
<%- if visual? -%>
|
|
51
|
+
check_page @account_page
|
|
52
|
+
<%- else -%>
|
|
53
|
+
@account_page.<% if capybara? -%>visit_page<% else -%>visit<% end %>
|
|
54
|
+
assert_equal "Welcome back #{@user['name']}", @account_page.header.customer_name
|
|
55
|
+
<%- end -%>
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_login_with_wrong_credentials
|
|
59
|
+
@login_page.<% if capybara? -%>visit_page<% else -%>visit<% end %>
|
|
60
|
+
@login_page.login(@user['username'], 'wrongPassword')
|
|
61
|
+
<%- if visual? -%>
|
|
62
|
+
check_page @login_page
|
|
63
|
+
<%- else -%>
|
|
64
|
+
@login_page.<% if capybara? -%>visit_page<% else -%>visit<% end %>
|
|
65
|
+
assert_equal 'Login or register', @account_page.header.customer_name
|
|
66
|
+
<%- end -%>
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
<%- end -%>
|
|
70
|
+
<%- else -%>
|
|
71
|
+
<% if cross_platform? -%>
|
|
72
|
+
require_relative '../helpers/appium_helper'
|
|
73
|
+
<%- end -%>
|
|
74
|
+
require_relative '../helpers/test_helper'
|
|
75
|
+
require_relative '../page_objects/pages/home'
|
|
76
|
+
require_relative '../page_objects/pages/pdp'
|
|
77
|
+
|
|
78
|
+
class TestPdp < Minitest::Test
|
|
79
|
+
def setup
|
|
80
|
+
@home_page = Home.new(driver)
|
|
81
|
+
@pdp_page = Pdp.new(driver)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def teardown
|
|
85
|
+
driver.quit_driver
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_shows_add_to_cart_button
|
|
89
|
+
@home_page.go_to_product_detail
|
|
90
|
+
assert_equal 'Add to Cart', @pdp_page.add_to_cart_text
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
<%- end -%>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%- if selenium_based? || watir? -%>
|
|
1
|
+
<%- if selenium_based? || watir? || capybara? -%>
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require_relative '../helpers/spec_helper'
|
|
@@ -23,26 +23,26 @@ describe 'Login' do
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
it 'no accessibility errors are present on the transaction history' do
|
|
26
|
-
|
|
27
|
-
expect(account.page).to be_axe_clean.within
|
|
26
|
+
account_details = '.account-card'
|
|
27
|
+
expect(account.page).to be_axe_clean.within account_details
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
it 'no accessibility errors are present on the heading' do
|
|
31
|
-
heading = '.
|
|
31
|
+
heading = '.page-title'
|
|
32
32
|
expect(account.page).to be_axe_clean.within heading
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
-
<%- elsif selenium_based? || watir? -%>
|
|
36
|
+
<%- elsif selenium_based? || watir? || capybara? -%>
|
|
37
37
|
describe 'Login' do
|
|
38
38
|
subject(:header) { account_page.header.customer_name }
|
|
39
39
|
|
|
40
40
|
let(:user) { ModelFactory.for('users')['registered user'] }
|
|
41
|
-
let(:login_page) { Login.new
|
|
42
|
-
let(:account_page) { Account.new
|
|
41
|
+
let(:login_page) { <% if capybara? -%>Login.new<% elsif watir? -%>Login.new(browser)<% else -%>Login.new(driver)<% end -%> }
|
|
42
|
+
let(:account_page) { <% if capybara? -%>Account.new<% elsif watir? -%>Account.new(browser)<% else -%>Account.new(driver)<% end -%> }
|
|
43
43
|
|
|
44
44
|
before do
|
|
45
|
-
login_page
|
|
45
|
+
login_page.<% if capybara? -%>visit_page<% else -%>visit<% end %>
|
|
46
46
|
login_page.login(user['username'], password)
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -53,6 +53,7 @@ describe 'Login' do
|
|
|
53
53
|
<%- if visual? -%>
|
|
54
54
|
check_page account_page
|
|
55
55
|
<%- else -%>
|
|
56
|
+
account_page.<% if capybara? -%>visit_page<% else -%>visit<% end %>
|
|
56
57
|
expect(header).to eq "Welcome back #{user['name']}"
|
|
57
58
|
<%- end -%>
|
|
58
59
|
end
|
|
@@ -65,6 +66,7 @@ describe 'Login' do
|
|
|
65
66
|
<%- if visual? -%>
|
|
66
67
|
check_page login_page
|
|
67
68
|
<%- else -%>
|
|
69
|
+
login_page.<% if capybara? -%>visit_page<% else -%>visit<% end %>
|
|
68
70
|
expect(header).to eq 'Login or register'
|
|
69
71
|
<%- end -%>
|
|
70
72
|
end
|
|
@@ -86,8 +88,8 @@ class PdpSpec
|
|
|
86
88
|
let(:pdp_page) { Pdp.new(driver) }
|
|
87
89
|
|
|
88
90
|
it 'shows add to cart button' do
|
|
89
|
-
home_page.
|
|
90
|
-
expect(pdp_page.add_to_cart_text).to eq 'Add
|
|
91
|
+
home_page.go_to_product_detail
|
|
92
|
+
expect(pdp_page.add_to_cart_text).to eq 'Add to Cart'
|
|
91
93
|
end
|
|
92
94
|
end
|
|
93
95
|
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require_relative 'partial_resolver'
|
|
5
|
+
require_relative 'template_error'
|
|
6
|
+
|
|
7
|
+
module TemplateRenderer
|
|
8
|
+
# Caches compiled ERB template objects with mtime-based invalidation
|
|
9
|
+
#
|
|
10
|
+
# Cache structure:
|
|
11
|
+
# cache_key => { erb: ERB_object, mtime: Time, path: String }
|
|
12
|
+
#
|
|
13
|
+
# Cache keys include trim_mode to support different whitespace handling:
|
|
14
|
+
# "screenshot:-" => ERB object with trim_mode: '-'
|
|
15
|
+
# "screenshot:" => ERB object with no trim_mode
|
|
16
|
+
#
|
|
17
|
+
# Performance: ~10x speedup on cached renders (135ms → ~13.5ms)
|
|
18
|
+
class PartialCache
|
|
19
|
+
def initialize(generator_class)
|
|
20
|
+
@cache = {}
|
|
21
|
+
@resolver = PartialResolver.new(generator_class)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Render a partial with caching
|
|
25
|
+
#
|
|
26
|
+
# @param name [String] Partial name (without .tt extension)
|
|
27
|
+
# @param binding [Binding] Binding context for ERB evaluation
|
|
28
|
+
# @param options [Hash] Rendering options
|
|
29
|
+
# @option options [String, nil] :trim_mode ERB trim mode ('-', '<>', etc.)
|
|
30
|
+
# @return [String] Rendered template content
|
|
31
|
+
# @raise [TemplateNotFoundError] If partial not found
|
|
32
|
+
# @raise [TemplateRenderError] If rendering fails
|
|
33
|
+
def render_partial(name, binding, options = {})
|
|
34
|
+
trim_mode = options[:trim_mode]
|
|
35
|
+
cache_key = build_cache_key(name, trim_mode)
|
|
36
|
+
|
|
37
|
+
# Resolve the partial path
|
|
38
|
+
path = @resolver.resolve(name, binding)
|
|
39
|
+
|
|
40
|
+
# Get from cache or compile
|
|
41
|
+
erb = get_or_compile(cache_key, path, trim_mode)
|
|
42
|
+
|
|
43
|
+
# Render with provided binding
|
|
44
|
+
erb.result(binding)
|
|
45
|
+
rescue Errno::ENOENT => e
|
|
46
|
+
raise TemplateNotFoundError.new(
|
|
47
|
+
"Partial '#{name}' not found",
|
|
48
|
+
partial_name: name,
|
|
49
|
+
searched_paths: @resolver.search_paths(name, binding),
|
|
50
|
+
original_error: e
|
|
51
|
+
)
|
|
52
|
+
rescue TemplateError
|
|
53
|
+
raise
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
# Catch ERB syntax errors or other rendering issues
|
|
56
|
+
raise TemplateRenderError.new(
|
|
57
|
+
e.message,
|
|
58
|
+
partial_name: name,
|
|
59
|
+
original_error: e
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Clear the entire cache (useful for testing)
|
|
64
|
+
def clear
|
|
65
|
+
@cache.clear
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get cache statistics (useful for debugging/monitoring)
|
|
69
|
+
def stats
|
|
70
|
+
{
|
|
71
|
+
size: @cache.size,
|
|
72
|
+
entries: @cache.keys,
|
|
73
|
+
memory_estimate: estimate_cache_size
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Build cache key that includes trim_mode
|
|
80
|
+
def build_cache_key(name, trim_mode)
|
|
81
|
+
"#{name}:#{trim_mode}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get cached ERB or compile and cache it
|
|
85
|
+
def get_or_compile(cache_key, path, trim_mode)
|
|
86
|
+
cached = @cache[cache_key]
|
|
87
|
+
current_mtime = File.mtime(path)
|
|
88
|
+
|
|
89
|
+
# Cache miss or stale cache (file modified)
|
|
90
|
+
if cached.nil? || cached[:mtime] < current_mtime
|
|
91
|
+
erb = compile_template(path, trim_mode)
|
|
92
|
+
@cache[cache_key] = {
|
|
93
|
+
erb:,
|
|
94
|
+
mtime: current_mtime,
|
|
95
|
+
path:
|
|
96
|
+
}
|
|
97
|
+
return erb
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Cache hit
|
|
101
|
+
cached[:erb]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Compile an ERB template
|
|
105
|
+
def compile_template(path, trim_mode)
|
|
106
|
+
content = File.read(path)
|
|
107
|
+
ERB.new(content, trim_mode:)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Rough estimate of cache memory usage
|
|
111
|
+
# Each entry: ~2-10 KB (ERB object + metadata)
|
|
112
|
+
def estimate_cache_size
|
|
113
|
+
@cache.size * 5 * 1024 # Rough estimate: 5 KB per entry
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'template_error'
|
|
4
|
+
|
|
5
|
+
module TemplateRenderer
|
|
6
|
+
# Resolves partial template paths with context-aware searching
|
|
7
|
+
#
|
|
8
|
+
# Resolution strategy:
|
|
9
|
+
# 1. Try relative to calling template: ./partials/{name}.tt
|
|
10
|
+
# 2. Fall back to all Generator.source_paths searching for partials/{name}.tt
|
|
11
|
+
class PartialResolver
|
|
12
|
+
PARTIAL_EXTENSION = '.tt'
|
|
13
|
+
PARTIALS_DIR = 'partials'
|
|
14
|
+
|
|
15
|
+
def initialize(generator_class)
|
|
16
|
+
@generator_class = generator_class
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Resolve a partial name to its absolute file path
|
|
20
|
+
#
|
|
21
|
+
# @param name [String] The partial name (without .tt extension)
|
|
22
|
+
# @param binding [Binding] The binding context from the caller
|
|
23
|
+
# @return [String] Absolute path to the partial file
|
|
24
|
+
# @raise [TemplateNotFoundError] If partial cannot be found
|
|
25
|
+
def resolve(name, binding)
|
|
26
|
+
caller_file = caller_file_from_binding(binding)
|
|
27
|
+
partial_filename = "#{name}#{PARTIAL_EXTENSION}"
|
|
28
|
+
|
|
29
|
+
# Try relative to caller first
|
|
30
|
+
if caller_file
|
|
31
|
+
relative_path = File.join(File.dirname(caller_file), PARTIALS_DIR, partial_filename)
|
|
32
|
+
return File.expand_path(relative_path) if File.exist?(relative_path)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Fall back to searching all source paths
|
|
36
|
+
searched = search_source_paths(partial_filename)
|
|
37
|
+
return searched[:found] if searched[:found]
|
|
38
|
+
|
|
39
|
+
# Not found - raise with helpful error
|
|
40
|
+
raise TemplateNotFoundError.new(
|
|
41
|
+
"Partial '#{name}' not found",
|
|
42
|
+
partial_name: name,
|
|
43
|
+
searched_paths: search_paths(name, binding)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get all paths that were searched (for error messages)
|
|
48
|
+
def search_paths(name, binding)
|
|
49
|
+
paths = []
|
|
50
|
+
partial_filename = "#{name}#{PARTIAL_EXTENSION}"
|
|
51
|
+
|
|
52
|
+
# Add relative path if available
|
|
53
|
+
caller_file = caller_file_from_binding(binding)
|
|
54
|
+
if caller_file
|
|
55
|
+
relative_path = File.join(File.dirname(caller_file), PARTIALS_DIR, partial_filename)
|
|
56
|
+
paths << relative_path
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Add all source path possibilities (including subdirectories)
|
|
60
|
+
source_paths.each do |source_path|
|
|
61
|
+
paths << File.join(source_path, PARTIALS_DIR, partial_filename)
|
|
62
|
+
|
|
63
|
+
# Also include subdirectories (e.g., templates/common/partials/, templates/helpers/partials/)
|
|
64
|
+
Dir.glob(File.join(source_path, '*', PARTIALS_DIR)).each do |subdir|
|
|
65
|
+
paths << File.join(subdir, partial_filename)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
paths
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Extract the calling template file from binding context
|
|
75
|
+
def caller_file_from_binding(binding)
|
|
76
|
+
binding.eval('__FILE__')
|
|
77
|
+
rescue StandardError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Search all Generator.source_paths for the partial
|
|
82
|
+
# Checks both source_path/partials/ and source_path/*/partials/
|
|
83
|
+
def search_source_paths(partial_filename)
|
|
84
|
+
source_paths.each do |source_path|
|
|
85
|
+
# First try direct partials directory
|
|
86
|
+
full_path = File.join(source_path, PARTIALS_DIR, partial_filename)
|
|
87
|
+
return { found: File.expand_path(full_path), searched: [full_path] } if File.exist?(full_path)
|
|
88
|
+
|
|
89
|
+
# Then try subdirectories (e.g., templates/common/partials/, templates/helpers/partials/)
|
|
90
|
+
Dir.glob(File.join(source_path, '*', PARTIALS_DIR, partial_filename)).each do |path|
|
|
91
|
+
return { found: File.expand_path(path), searched: [path] } if File.exist?(path)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
{ found: nil, searched: [] }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get all configured source paths from the generator class
|
|
99
|
+
def source_paths
|
|
100
|
+
@source_paths ||= @generator_class.source_paths || []
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TemplateRenderer
|
|
4
|
+
# Base error class for all template-related errors
|
|
5
|
+
class TemplateError < StandardError
|
|
6
|
+
attr_reader :partial_name, :searched_paths, :original_error
|
|
7
|
+
|
|
8
|
+
def initialize(message, partial_name: nil, searched_paths: nil, original_error: nil)
|
|
9
|
+
@partial_name = partial_name
|
|
10
|
+
@searched_paths = searched_paths || []
|
|
11
|
+
@original_error = original_error
|
|
12
|
+
super(message)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Raised when a partial template cannot be found
|
|
17
|
+
class TemplateNotFoundError < TemplateError
|
|
18
|
+
def to_s
|
|
19
|
+
message_parts = ["Partial '#{@partial_name}' not found."]
|
|
20
|
+
|
|
21
|
+
if @searched_paths.any?
|
|
22
|
+
message_parts << "\nSearched in:"
|
|
23
|
+
message_parts.concat(@searched_paths.map { |path| " - #{path}" })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
message_parts.join("\n")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Raised when a template has syntax errors or rendering fails
|
|
31
|
+
class TemplateRenderError < TemplateError
|
|
32
|
+
def initialize(message, partial_name:, original_error: nil)
|
|
33
|
+
super(message, partial_name:, original_error:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
message_parts = ["Error rendering partial '#{@partial_name}': #{super()}"]
|
|
38
|
+
|
|
39
|
+
if @original_error
|
|
40
|
+
message_parts << "\nOriginal error: #{@original_error.class}: #{@original_error.message}"
|
|
41
|
+
if @original_error.backtrace
|
|
42
|
+
message_parts << "\nBacktrace:"
|
|
43
|
+
message_parts.concat(@original_error.backtrace.first(5).map { |line| " #{line}" })
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
message_parts.join("\n")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|