diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f860dd5ae..8dfd83dfd 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-versions: ['8.2', '8.3', '8.4']
+ php-versions: ["8.2", "8.3", "8.4"]
steps:
- uses: actions/checkout@v2
@@ -31,7 +31,7 @@ jobs:
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
-
+
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
@@ -39,14 +39,14 @@ jobs:
- name: Run tests
run: vendor/bin/phpunit --configuration dev/tests/phpunit.xml --testsuite unit --coverage-clover clover.xml
-# - name: Monitor coverage
-# if: github.event_name == 'pull_request'
-# uses: slavcodev/coverage-monitor-action@1.2.0
-# with:
-# github_token: ${{ secrets.GITHUB_TOKEN }}
-# clover_file: "clover.xml"
-# threshold_alert: 10
-# threshold_warning: 20
+ # - name: Monitor coverage
+ # if: github.event_name == 'pull_request'
+ # uses: slavcodev/coverage-monitor-action@1.2.0
+ # with:
+ # github_token: ${{ secrets.GITHUB_TOKEN }}
+ # clover_file: "clover.xml"
+ # threshold_alert: 10
+ # threshold_warning: 20
verification-tests:
name: Verification Tests
@@ -54,7 +54,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-versions: ['8.2', '8.3', '8.4']
+ php-versions: ["8.2", "8.3", "8.4"]
steps:
- uses: actions/checkout@v2
@@ -72,7 +72,7 @@ jobs:
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
-
+
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
@@ -86,7 +86,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-versions: ['8.2', '8.3', '8.4']
+ php-versions: ["8.2", "8.3", "8.4"]
steps:
- uses: actions/checkout@v2
@@ -104,7 +104,7 @@ jobs:
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
-
+
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
@@ -118,7 +118,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-versions: ['8.2', '8.3', '8.4']
+ php-versions: ["8.2", "8.3", "8.4"]
services:
chrome:
@@ -126,7 +126,7 @@ jobs:
ports:
- 4444:4444
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@master
@@ -136,13 +136,13 @@ jobs:
- name: Cache Composer packages
id: composer-cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
-
+
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
@@ -150,4 +150,81 @@ jobs:
- name: Run tests
run: bin/functional
+ testrigor-tests:
+ name: testRigor Tests
+ runs-on: ubuntu-latest
+
+ env:
+ # testRigor variables
+ MAGENTO_TEST_SUITE_ID: ${{vars.MAGENTO_TEST_SUITE_ID}}
+ MAGENTO_AUTH_TOKEN: ${{secrets.MAGENTO_AUTH_TOKEN}}
+
+ # MFTF Magento connection variables
+ MAGENTO_BASE_URL: ${{vars.MAGENTO_BASE_URL}}
+ MAGENTO_BACKEND_NAME: ${{vars.MAGENTO_BACKEND_NAME}}
+ MAGENTO_ADMIN_USERNAME: ${{vars.MAGENTO_ADMIN_USERNAME}}
+ MAGENTO_ADMIN_PASSWORD: ${{secrets.MAGENTO_ADMIN_PASSWORD}}
+
+ # MFTF configuration
+ SELENIUM_CLOSE_ALL_SESSIONS: true
+ BROWSER: chrome
+ WINDOW_WIDTH: 1920
+ WINDOW_HEIGHT: 1080
+ WAIT_TIMEOUT: 60
+ MAGENTO_CLI_WAIT_TIMEOUT: 60
+ TEST_ENV: ci
+ HEADLESS: true
+ services:
+ chrome:
+ image: selenium/standalone-chrome:3.141.59-zirconium
+ ports:
+ - 4444:4444
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@master
+ with:
+ php-version: "8.2"
+ extensions: curl, dom, intl, json, openssl, zip
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v4
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ if: steps.composer-cache.outputs.cache-hit != 'true'
+ run: composer install --prefer-dist --no-progress --no-suggest
+
+ - name: Verify Magento connection
+ run: |
+ echo "Testing connection to $MAGENTO_BASE_URL"
+ curl -f -s -o /dev/null $MAGENTO_BASE_URL || echo "Warning: Cannot connect to Magento"
+
+ - name: Build MFTF project
+ run: php bin/mftf build:project
+
+ - name: Verify MFTF configuration
+ run: php bin/mftf doctor || true
+
+ - name: Run testRigor tests
+ run: |
+ echo "Running MFTF testRigor tests..."
+ php bin/mftf run:testrigor
+
+ - name: Upload test artifacts
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: mftf-test-artifacts
+ path: |
+ dev/tests/acceptance/_output/
+ var/log/
+ retention-days: 7
diff --git a/.gitignore b/.gitignore
index 419218ba4..37d1f7c6c 100755
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
composer.phar
vendor/*
.env
+node_modules/
+package-lock.json
+.mftf-tools/
_generated
AcceptanceTester.php
cghooks.lock
diff --git a/bin/mftf b/bin/mftf
index 7a9ca1cf2..dd927e92c 100755
--- a/bin/mftf
+++ b/bin/mftf
@@ -25,7 +25,6 @@ try {
exit(1);
}
-
try {
$version = json_decode(file_get_contents(FW_BP . DIRECTORY_SEPARATOR . 'composer.json'), true);
$version = $version['version'];
diff --git a/bin/testrigor b/bin/testrigor
new file mode 100644
index 000000000..a2ad124a2
--- /dev/null
+++ b/bin/testrigor
@@ -0,0 +1,12 @@
+#!/bin/bash
+echo "==============================="
+echo " EXECUTE testRigor Tests "
+echo "==============================="
+
+echo "Building MFTF project..."
+bin/mftf build:project
+
+echo "$MAGENTO_BASE_URL"
+
+echo "Running testRigor tests..."
+chmod +x ./dev/tests/testRigor/testrigor && ./dev/tests/testRigor/testrigor
\ No newline at end of file
diff --git a/dev/tests/testRigor/README.md b/dev/tests/testRigor/README.md
new file mode 100644
index 000000000..d2b6f13dd
--- /dev/null
+++ b/dev/tests/testRigor/README.md
@@ -0,0 +1,105 @@
+# Test Automation with testRigor for Magento
+
+This document provides step-by-step instructions for setting up test automation for Magento using testRigor. It covers creating an account, setting up a test suite, and running tests using the testRigor CLI.
+
+## Table of Contents
+
+- [Creating an Account on testRigor](#creating-an-account-on-testrigor)
+- [Running Tests with the CLI](#running-tests-with-the-cli)
+- [Additional Resources](#additional-resources)
+
+## Creating an Account on testRigor
+
+1. **Visit the testRigor website:**
+
+ - Go to [testRigor](https://www.testrigor.com/).
+
+2. **Sign up for a new account:**
+
+ - Click on the "Sign Up" button on the top right corner.
+ - Select the "Public Open Source" version.
+ - Fill in the required details and follow the instructions to complete the registration.
+
+3. **Verify your email and log in:**
+
+ - Check your email inbox for a verification email from testRigor.
+ - Click on the verification link to activate your account.
+ - Once your account is activated, log in.
+
+4. **Create a test suite:**
+ - After logging into your account, create a test suite.
+
+## Running Tests with the CLI
+
+1. **Prerequisites:**
+
+ - **None!** MFTF will automatically download and install all required dependencies (Node.js and TestRigor CLI) on first run.
+ - Everything is installed locally within the project, requiring zero manual setup.
+ - The framework is fully self-contained and ready for CI/CD environments.
+
+2. **Obtain Required Parameters:**
+
+ - **Test Suite ID:** You can obtain the Test Suite ID in the URL of your test suite. If the URL is `https://app.testrigor.com/test-suites/12345`, then `12345` is your Test Suite ID.
+ - **Auth Token:** You can obtain your token from the "CI/CD integration" section on testRigor. Look for "auth-token" and copy the value next to it, which will be in the format `########-####-####-####-############`.
+
+3. **Set Parameters in `.env` file:**
+
+ - Before running the tests, create a .env file on the testRigor directory and set the following variables to the parameters you obtained:
+ - `MAGENTO_TEST_SUITE_ID`: Set this variable to your Test Suite ID.
+ - `MAGENTO_AUTH_TOKEN`: Set this variable to your auth token.
+ - `MAGENTO_BASE_URL`: Set this variable to the URL where Magento is running locally.
+
+ Example `.env` file:
+ ```
+ MAGENTO_TEST_SUITE_ID=12345
+ MAGENTO_AUTH_TOKEN=########-####-####-####-############
+ MAGENTO_BASE_URL=http://localhost:8080
+ ```
+
+4. **Run Tests:**
+
+ ```bash
+ bin/mftf run:testrigor
+ ```
+
+ The command will:
+ - Automatically check if Node.js and TestRigor CLI are installed
+ - Download and install them locally if not found (no manual installation needed!)
+ - Load environment variables from the `.env` file in the project root
+ - Execute your TestRigor test suite
+
+5. **View Test Results:**
+ - You can view the results on testRigor by opening the link shown in the terminal.
+
+## Troubleshooting
+
+### Automatic Installation Issues
+
+MFTF handles all installations automatically. If you encounter issues:
+
+1. **Check for curl:**
+ ```bash
+ curl --version
+ ```
+ The framework uses `curl` to download Node.js. Most systems have it pre-installed.
+
+2. **Verify disk space:**
+ Ensure you have at least 100MB of free disk space for Node.js and dependencies.
+
+3. **Check file permissions:**
+ The framework creates a `.mftf-tools` directory in the project root. Ensure the directory is writable.
+
+4. **Manual cleanup:**
+ If installation fails, try removing cached files:
+ ```bash
+ rm -rf .mftf-tools node_modules
+ ```
+ Then run the command again.
+
+5. **Using system Node.js:**
+ If you already have Node.js 18+ installed system-wide, MFTF will detect and use it automatically, skipping the download.
+
+## Additional Resources
+
+- [testRigor Documentation](https://docs.testrigor.com/)
+- [testRigor Command Line Documentation](https://testrigor.com/command-line/)
diff --git a/dev/tests/testRigor/rules/add_any_item_to_the_cart.txt b/dev/tests/testRigor/rules/add_any_item_to_the_cart.txt
new file mode 100644
index 000000000..ff3778302
--- /dev/null
+++ b/dev/tests/testRigor/rules/add_any_item_to_the_cart.txt
@@ -0,0 +1,6 @@
+click "New Luma Yoga Collection Get fit and look fab in new seasonal styles Shop New Yoga"
+click "Ida Workout Parachute Pant"
+click "28"
+click "Blue"
+click "Add to Cart"
+check if page contains "Add to Cart"
diff --git a/dev/tests/testRigor/rules/create_an_account.txt b/dev/tests/testRigor/rules/create_an_account.txt
new file mode 100644
index 000000000..682dd20ed
--- /dev/null
+++ b/dev/tests/testRigor/rules/create_an_account.txt
@@ -0,0 +1,9 @@
+click "Create an Account"
+generate from template "%$$$$$$", then enter into "First Name" and save it as "firstName"
+generate from template "%$$$$$$", then enter into "Last Name" and save it as "lastName"
+generate unique email, then enter into "Email" and save as "Email"
+enter stored value "password" into "Password"
+enter stored value "password" into "Confirm Password"
+click "Create an Account" below "Confirm Password"
+validate that page contains "Thank you for registering"
+validate that page contains string with parameters "Welcome, ${firstName} ${lastName}"
diff --git a/dev/tests/testRigor/rules/proceed_to_checkout.txt b/dev/tests/testRigor/rules/proceed_to_checkout.txt
new file mode 100644
index 000000000..76b73d48c
--- /dev/null
+++ b/dev/tests/testRigor/rules/proceed_to_checkout.txt
@@ -0,0 +1,10 @@
+click "Proceed to Checkout"
+wait 1 sec until page contains "Shipping Address"
+fill out required fields of form "Shipping Address" with generated values
+select "Alabama" from "State/Province"
+generate from template "#####", then enter into field "Zip/Postal code"
+generate from template "#########", then enter into field "Phone Number"
+click "$" roughly below "Shipping methods"
+validate that page contains button "Next"
+click "Next"
+click "Place Order"
diff --git a/dev/tests/testRigor/rules/select_an_item_from_the_search_results.txt b/dev/tests/testRigor/rules/select_an_item_from_the_search_results.txt
new file mode 100644
index 000000000..bab14078a
--- /dev/null
+++ b/dev/tests/testRigor/rules/select_an_item_from_the_search_results.txt
@@ -0,0 +1,2 @@
+scroll down
+click "Cronus Yoga Pant"
diff --git a/dev/tests/testRigor/rules/select_josie_yoga_jacket_on_size_xs_and_color_blue,_and_add_it_to_the_cart.txt b/dev/tests/testRigor/rules/select_josie_yoga_jacket_on_size_xs_and_color_blue,_and_add_it_to_the_cart.txt
new file mode 100644
index 000000000..8cdf14de3
--- /dev/null
+++ b/dev/tests/testRigor/rules/select_josie_yoga_jacket_on_size_xs_and_color_blue,_and_add_it_to_the_cart.txt
@@ -0,0 +1,4 @@
+click "Josie Yoga Jacket"
+click "XS"
+click "Blue"
+click "Add to Cart"
diff --git a/dev/tests/testRigor/rules/select_size_and_color.txt b/dev/tests/testRigor/rules/select_size_and_color.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/dev/tests/testRigor/testcases/Before_testing.txt b/dev/tests/testRigor/testcases/Before_testing.txt
new file mode 100644
index 000000000..4032f9805
--- /dev/null
+++ b/dev/tests/testRigor/testcases/Before_testing.txt
@@ -0,0 +1 @@
+validate that page contains "Default welcome msg!"
diff --git a/dev/tests/testRigor/testcases/Log_in,_search_for_an_item,_add_it_to_the_cart_and_finish_the_purchase_with_a_credit_card_and_address..txt b/dev/tests/testRigor/testcases/Log_in,_search_for_an_item,_add_it_to_the_cart_and_finish_the_purchase_with_a_credit_card_and_address..txt
new file mode 100644
index 000000000..064c55a4f
--- /dev/null
+++ b/dev/tests/testRigor/testcases/Log_in,_search_for_an_item,_add_it_to_the_cart_and_finish_the_purchase_with_a_credit_card_and_address..txt
@@ -0,0 +1,8 @@
+create an account
+open url saved value "homePrefix"
+add any item to the cart
+scroll up until page contains "shopping cart"
+click button "shopping cart"
+proceed to checkout
+validate that page contains "Thank you for your purchase!"
+validate that page has regex "Your order number is: [0-9]{9}"
diff --git a/dev/tests/testRigor/testcases/Validate_the_ability_to_search_products_by_category_and_validate_accurate_results_are_displayed_based_on_the_selected_filter..txt b/dev/tests/testRigor/testcases/Validate_the_ability_to_search_products_by_category_and_validate_accurate_results_are_displayed_based_on_the_selected_filter..txt
new file mode 100644
index 000000000..e31091a6a
--- /dev/null
+++ b/dev/tests/testRigor/testcases/Validate_the_ability_to_search_products_by_category_and_validate_accurate_results_are_displayed_based_on_the_selected_filter..txt
@@ -0,0 +1,6 @@
+click "Women"
+click "Bras & Tanks"
+check if page contains "Tank" above "$" and roughly below "Shopping Options"
+scroll down until page contains "Page 2"
+click "Page 2"
+check if page contains "Bra" above "$" and roughly below "Shopping Options"
diff --git a/dev/tests/testRigor/testcases/Validate_the_ability_to_select_a_size_and_color_for_a_product_and_add_it_to_the_cart_without_logging_in..txt b/dev/tests/testRigor/testcases/Validate_the_ability_to_select_a_size_and_color_for_a_product_and_add_it_to_the_cart_without_logging_in..txt
new file mode 100644
index 000000000..ce8d78290
--- /dev/null
+++ b/dev/tests/testRigor/testcases/Validate_the_ability_to_select_a_size_and_color_for_a_product_and_add_it_to_the_cart_without_logging_in..txt
@@ -0,0 +1,8 @@
+hover over "Women"
+click "Tops"
+click "Breathe-Easy Tank"
+click "XS"
+click "Yellow"
+click "Add to Cart"
+click button "shopping cart"
+validate that page contains "Breathe-Easy tank" roughly below "Item"
diff --git a/dev/tests/testRigor/testcases/Validate_the_functionality_to_view,_without_logging_in,_the_details_of_a_searched_product,_including_prices,_available_sizes_and_colors,_and_zooming_in_product_image,_and_validate_the_information_is_displayed_correctly._....txt b/dev/tests/testRigor/testcases/Validate_the_functionality_to_view,_without_logging_in,_the_details_of_a_searched_product,_including_prices,_available_sizes_and_colors,_and_zooming_in_product_image,_and_validate_the_information_is_displayed_correctly._....txt
new file mode 100644
index 000000000..031892e87
--- /dev/null
+++ b/dev/tests/testRigor/testcases/Validate_the_functionality_to_view,_without_logging_in,_the_details_of_a_searched_product,_including_prices,_available_sizes_and_colors,_and_zooming_in_product_image,_and_validate_the_information_is_displayed_correctly._....txt
@@ -0,0 +1,9 @@
+search for "yoga pants"
+select an item from the search results
+check if page contains "Qty"
+check if page contains "Size"
+check if page contains "Color"
+check if page contains "$"
+click "product media"
+check that "zoom-in" is visible
+check that "zoom-out" is visible
diff --git a/dev/tests/testRigor/testcases/Verify_the_ability_log_in,_search_a_product,_add_it_to_the_wishlist_and_validate_they_are_displayed_correctly_in_the_wishlist_section.txt b/dev/tests/testRigor/testcases/Verify_the_ability_log_in,_search_a_product,_add_it_to_the_wishlist_and_validate_they_are_displayed_correctly_in_the_wishlist_section.txt
new file mode 100644
index 000000000..406545dcd
--- /dev/null
+++ b/dev/tests/testRigor/testcases/Verify_the_ability_log_in,_search_a_product,_add_it_to_the_wishlist_and_validate_they_are_displayed_correctly_in_the_wishlist_section.txt
@@ -0,0 +1,9 @@
+create an account
+search for Water bottle
+click link "water bottle"
+validate that page contains "$" roughly above "Add to Cart"
+click "Add to wish list" roughly below "Add to cart"
+validate that page contains "Added"
+click string with parameters "${firstName} ${lastName}" on the right of string with parameters "Welcome, ${firstName} ${lastName}"
+click "My Wish List"
+validate that page contains "Water bottle" roughly below "My Wish List"
diff --git a/dev/tests/testRigor/testcases/Verify_the_functionality_to_compare_two_or_more_products_without_logging_in_and_ensure_a_detailed_comparison_is_displayed..txt b/dev/tests/testRigor/testcases/Verify_the_functionality_to_compare_two_or_more_products_without_logging_in_and_ensure_a_detailed_comparison_is_displayed..txt
new file mode 100644
index 000000000..182b22599
--- /dev/null
+++ b/dev/tests/testRigor/testcases/Verify_the_functionality_to_compare_two_or_more_products_without_logging_in_and_ensure_a_detailed_comparison_is_displayed..txt
@@ -0,0 +1,18 @@
+hover over "Gear"
+click "Fitness Equipment"
+scroll down until page contains "Band Kit"
+click "Band Kit"
+click "Add to compare" roughly below "Add to cart"
+validate page contains "Compare Products (1 item)" roughly on the left of "Search"
+go back until page contains "Fitness equipment" roughly above "Shopping Options"
+refresh
+scroll down until page contains "Tone band"
+click "Tone Band"
+check that page contains "Add to compare" roughly below "Add to cart"
+click "Add to compare" roughly below "Add to cart"
+validate page contains "Compare Products (2 items)" roughly on the left of "Search"
+click "Compare Products (2 items)"
+validate that page contains "Compare products"
+validate that page contains "Activity" below "Description"
+validate that page contains "Band Kit"
+validate that page contains "Tone Band"
diff --git a/dev/tests/testRigor/testcases/Verify_the_functionality_to_write_and_submit_rated_reviews_on_product_pages_and_ensure_the_reviews_are_displayed_correctly..txt b/dev/tests/testRigor/testcases/Verify_the_functionality_to_write_and_submit_rated_reviews_on_product_pages_and_ensure_the_reviews_are_displayed_correctly..txt
new file mode 100644
index 000000000..01c61a452
--- /dev/null
+++ b/dev/tests/testRigor/testcases/Verify_the_functionality_to_write_and_submit_rated_reviews_on_product_pages_and_ensure_the_reviews_are_displayed_correctly..txt
@@ -0,0 +1,9 @@
+search for "Bag"
+click "Bag" roughly below "Items"
+click "Add your review"
+validate "You're reviewing:" is visible
+validate "Submit Review" is visible
+click "4 stars" using ai
+fill out form
+click "Submit Review"
+validate that page contains "You submitted your review for moderation."
diff --git a/dev/tests/testRigor/testcases/create_account.txt b/dev/tests/testRigor/testcases/create_account.txt
new file mode 100644
index 000000000..225479f6c
--- /dev/null
+++ b/dev/tests/testRigor/testcases/create_account.txt
@@ -0,0 +1 @@
+create an account
diff --git a/dev/tests/testRigor/testcases/search_for_item.txt b/dev/tests/testRigor/testcases/search_for_item.txt
new file mode 100644
index 000000000..6c5139607
--- /dev/null
+++ b/dev/tests/testRigor/testcases/search_for_item.txt
@@ -0,0 +1,2 @@
+search for jacket
+validate that page contains "jacket"
diff --git a/src/Magento/FunctionalTestingFramework/Console/CommandList.php b/src/Magento/FunctionalTestingFramework/Console/CommandList.php
index a31ebe175..1b39cb298 100644
--- a/src/Magento/FunctionalTestingFramework/Console/CommandList.php
+++ b/src/Magento/FunctionalTestingFramework/Console/CommandList.php
@@ -41,6 +41,7 @@ public function __construct(array $commands = [])
'run:group' => new RunTestGroupCommand(),
'run:manifest' => new RunManifestCommand(),
'run:test' => new RunTestCommand(),
+ 'run:testrigor' => new RunTestRigorCommand(),
'setup:env' => new SetupEnvCommand(),
'static-checks' => new StaticChecksCommand(),
'upgrade:tests' => new UpgradeTestsCommand(),
diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestRigorCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestRigorCommand.php
new file mode 100644
index 000000000..25cbdaf4e
--- /dev/null
+++ b/src/Magento/FunctionalTestingFramework/Console/RunTestRigorCommand.php
@@ -0,0 +1,498 @@
+setName('run:testrigor')
+ ->setDescription('Run TestRigor tests against the Magento instance')
+ ->addOption(
+ 'force',
+ 'f',
+ InputOption::VALUE_NONE,
+ 'force execution regardless of Magento Instance Configuration'
+ );
+
+ // Initialize paths
+ // From Console directory, go up 4 levels to reach project root:
+ // Console -> FunctionalTestingFramework -> Magento -> src -> project root
+ $this->projectRoot = dirname(__DIR__, 4);
+ $this->toolsDir = $this->projectRoot . '/.mftf-tools';
+ $this->nodeDir = $this->toolsDir . '/node';
+ $this->nodeBin = $this->nodeDir . '/bin/node';
+ $this->npmBin = $this->nodeDir . '/bin/npm';
+ }
+
+ /**
+ * Detect the operating system and architecture
+ *
+ * @return array{os: string, arch: string}|null
+ */
+ protected function detectPlatform(): ?array
+ {
+ $os = strtolower(PHP_OS);
+ $arch = php_uname('m');
+
+ // Determine OS
+ if (stripos($os, 'linux') !== false) {
+ $osType = 'linux';
+ } elseif (stripos($os, 'darwin') !== false) {
+ $osType = 'darwin';
+ } elseif (stripos($os, 'win') !== false) {
+ $osType = 'win';
+ } else {
+ return null;
+ }
+
+ // Determine architecture
+ if (in_array($arch, ['x86_64', 'amd64', 'AMD64'])) {
+ $archType = 'x64';
+ } elseif (in_array($arch, ['aarch64', 'arm64'])) {
+ $archType = 'arm64';
+ } else {
+ $archType = 'x64'; // Default to x64
+ }
+
+ return ['os' => $osType, 'arch' => $archType];
+ }
+
+ /**
+ * Download and install Node.js locally
+ *
+ * @param OutputInterface $output
+ * @return bool
+ */
+ protected function installNodeJs(OutputInterface $output): bool
+ {
+ $platform = $this->detectPlatform();
+ if (!$platform) {
+ $output->writeln("Unsupported platform. Unable to auto-install Node.js.");
+ return false;
+ }
+
+ $output->writeln("Downloading Node.js v" . self::NODE_VERSION . "...");
+
+ // Create tools directory
+ if (!is_dir($this->toolsDir)) {
+ mkdir($this->toolsDir, 0755, true);
+ }
+
+ // Construct download URL
+ $nodeFileName = sprintf(
+ 'node-v%s-%s-%s',
+ self::NODE_VERSION,
+ $platform['os'],
+ $platform['arch']
+ );
+
+ if ($platform['os'] === 'win') {
+ $nodeFileName .= '.zip';
+ $downloadUrl = "https://nodejs.org/dist/v" . self::NODE_VERSION . "/" . $nodeFileName;
+ } else {
+ $nodeFileName .= '.tar.gz';
+ $downloadUrl = "https://nodejs.org/dist/v" . self::NODE_VERSION . "/" . $nodeFileName;
+ }
+
+ $downloadPath = $this->toolsDir . '/' . $nodeFileName;
+
+ // Download Node.js
+ $output->writeln("Downloading from: $downloadUrl");
+ $downloadCommand = sprintf(
+ 'curl -fsSL %s -o %s 2>&1',
+ escapeshellarg($downloadUrl),
+ escapeshellarg($downloadPath)
+ );
+
+ $downloadOutput = shell_exec($downloadCommand);
+ if (!file_exists($downloadPath)) {
+ $output->writeln("Failed to download Node.js");
+ if ($downloadOutput) {
+ $output->writeln($downloadOutput);
+ }
+ return false;
+ }
+
+ $output->writeln("✓ Downloaded Node.js");
+
+ // Extract Node.js
+ $output->writeln("Extracting Node.js...");
+ if ($platform['os'] === 'win') {
+ $extractCommand = sprintf(
+ 'unzip -q %s -d %s 2>&1',
+ escapeshellarg($downloadPath),
+ escapeshellarg($this->toolsDir)
+ );
+ } else {
+ $extractCommand = sprintf(
+ 'tar -xzf %s -C %s 2>&1',
+ escapeshellarg($downloadPath),
+ escapeshellarg($this->toolsDir)
+ );
+ }
+
+ shell_exec($extractCommand);
+
+ // Move extracted directory to nodeDir
+ $extractedDir = $this->toolsDir . '/' . str_replace(['.tar.gz', '.zip'], '', $nodeFileName);
+ if (is_dir($extractedDir)) {
+ if (is_dir($this->nodeDir)) {
+ // Remove old installation
+ shell_exec('rm -rf ' . escapeshellarg($this->nodeDir));
+ }
+ rename($extractedDir, $this->nodeDir);
+ unlink($downloadPath);
+
+ $output->writeln("✓ Node.js installed successfully!");
+ return true;
+ }
+
+ $output->writeln("Failed to extract Node.js");
+ return false;
+ }
+
+ /**
+ * Ensure Node.js is available (system or local installation)
+ *
+ * @param OutputInterface $output
+ * @return bool
+ */
+ protected function ensureNodeJs(OutputInterface $output): bool
+ {
+ // Check if we have a local Node.js installation
+ if (file_exists($this->nodeBin) && is_executable($this->nodeBin)) {
+ $version = shell_exec($this->nodeBin . ' --version 2>&1');
+ $output->writeln("✓ Using local Node.js: " . trim($version));
+ // When using local Node.js, we need to explicitly use it to run npm
+ // to avoid the system's old Node.js being used via shebang
+ $npmScript = $this->nodeDir . '/lib/node_modules/npm/bin/npm-cli.js';
+ if (file_exists($npmScript)) {
+ $this->npmBin = $this->nodeBin . ' ' . $npmScript;
+ }
+ return true;
+ }
+
+ // Check if system Node.js is available
+ $systemNodeCheck = 'command -v node > /dev/null 2>&1';
+ exec($systemNodeCheck, $checkOutput, $returnCode);
+
+ if ($returnCode === 0) {
+ $version = shell_exec('node --version 2>&1');
+ if ($version) {
+ preg_match('/v(\d+)\./', $version, $matches);
+ $majorVersion = isset($matches[1]) ? (int)$matches[1] : 0;
+ if ($majorVersion >= 18) {
+ $output->writeln("✓ Using system Node.js: " . trim($version));
+ // Update paths to use system binaries
+ $this->nodeBin = 'node';
+ $this->npmBin = 'npm';
+ return true;
+ } else {
+ $output->writeln("⚠ System Node.js version is too old (v$majorVersion), need v18+");
+ }
+ }
+ }
+
+ // No suitable Node.js found, install it locally
+ $output->writeln("Node.js not found. Installing locally...");
+ return $this->installNodeJs($output);
+ }
+
+ /**
+ * Install TestRigor CLI locally
+ *
+ * @param OutputInterface $output
+ * @return bool
+ */
+ protected function installTestRigorCli(OutputInterface $output): bool
+ {
+ $output->writeln("Installing TestRigor CLI...");
+
+ // Set up npm to install locally in project
+ $nodeModulesDir = $this->projectRoot . '/node_modules';
+ $testRigorBin = $nodeModulesDir . '/.bin/testrigor';
+
+ // Ensure NODE_PATH is set to use the correct node_modules
+ $envPath = 'NODE_PATH=' . escapeshellarg($nodeModulesDir);
+
+ // Install testrigor-cli with explicit npm prefix to force local installation
+ $package = self::TESTRIGOR_VERSION ? 'testrigor-cli@' . self::TESTRIGOR_VERSION : 'testrigor-cli';
+ $installCommand = sprintf(
+ 'cd %s && %s %s install --no-save %s 2>&1',
+ escapeshellarg($this->projectRoot),
+ $envPath,
+ $this->npmBin, // Don't escape since it might contain node + path
+ $package
+ );
+
+ $installOutput = shell_exec($installCommand);
+
+ if ($installOutput) {
+ // Check for actual errors (not warnings)
+ if (preg_match('/npm ERR!.*(?!WARN)/i', $installOutput)) {
+ $output->writeln("Failed to install TestRigor CLI:");
+ $output->writeln($installOutput);
+ return false;
+ }
+ }
+
+ // Verify installation - check multiple locations
+ if (file_exists($testRigorBin)) {
+ $output->writeln("✓ TestRigor CLI installed successfully at: $testRigorBin");
+ return true;
+ }
+
+ $altBin = $nodeModulesDir . '/testrigor-cli/bin/testrigor';
+ if (file_exists($altBin)) {
+ $output->writeln("✓ TestRigor CLI installed successfully at: $altBin");
+ return true;
+ }
+
+ if (is_dir($nodeModulesDir . '/testrigor-cli')) {
+ $output->writeln("✓ TestRigor CLI package installed");
+ return true;
+ }
+
+ $output->writeln("Failed to verify TestRigor CLI installation");
+ $output->writeln("Checked locations:");
+ $output->writeln(" - $testRigorBin");
+ $output->writeln(" - $altBin");
+ $output->writeln(" - $nodeModulesDir/testrigor-cli");
+ return false;
+ }
+
+ /**
+ * Get the testrigor binary path and command
+ *
+ * @return string|null
+ */
+ protected function getTestRigorBinary(): ?string
+ {
+ // Priority 1: Check local installation in node_modules (most reliable)
+ $localBin = $this->projectRoot . '/node_modules/.bin/testrigor';
+ if (file_exists($localBin) && is_executable($localBin)) {
+ // Use our Node.js to run it to ensure correct version
+ return $this->nodeBin . ' ' . $localBin;
+ }
+
+ // Priority 2: Check for direct access to the CLI script
+ $directPath = $this->projectRoot . '/node_modules/testrigor-cli/bin/testrigor';
+ if (file_exists($directPath)) {
+ return $this->nodeBin . ' ' . $directPath;
+ }
+
+ // Priority 3: Check for global testrigor command
+ $globalCheck = 'command -v testrigor > /dev/null 2>&1';
+ exec($globalCheck, $output, $returnCode);
+ if ($returnCode === 0) {
+ return 'testrigor';
+ }
+
+ // Priority 4: Check for global testrigor-cli command
+ $globalCheckCli = 'command -v testrigor-cli > /dev/null 2>&1';
+ exec($globalCheckCli, $outputCli, $returnCodeCli);
+ if ($returnCodeCli === 0) {
+ return 'testrigor-cli';
+ }
+
+ // Priority 5: Use npx with our Node.js if package is installed
+ if (is_dir($this->projectRoot . '/node_modules/testrigor-cli')) {
+ $npxBin = dirname($this->npmBin) . '/npx';
+ if (file_exists($npxBin)) {
+ return $npxBin . ' testrigor-cli';
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Ensure all dependencies are installed
+ *
+ * @param OutputInterface $output
+ * @return bool
+ */
+ protected function ensureTestRigorInstalled(OutputInterface $output): bool
+ {
+ // Step 1: Ensure Node.js is available
+ if (!$this->ensureNodeJs($output)) {
+ return false;
+ }
+
+ // Step 2: Check if TestRigor CLI is already available
+ $testRigorBin = $this->getTestRigorBinary();
+ if ($testRigorBin) {
+ $output->writeln("✓ TestRigor CLI is already installed");
+ return true;
+ }
+
+ // Step 3: Install TestRigor CLI
+ return $this->installTestRigorCli($output);
+ }
+
+ /**
+ * Executes the current command.
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return integer
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $force = $input->getOption('force');
+ $verbose = $output->isVerbose();
+
+ // Set application configuration
+ MftfApplicationConfig::create(
+ $force,
+ MftfApplicationConfig::EXECUTION_PHASE,
+ $verbose,
+ MftfApplicationConfig::LEVEL_DEFAULT,
+ false
+ );
+
+ $output->writeln('Running TestRigor Integration...');
+
+ // Get base URL from different possible sources
+ $baseUrl = null;
+ if (defined('MAGENTO_BASE_URL')) {
+ $baseUrl = MAGENTO_BASE_URL;
+ } elseif (getenv('MAGENTO_BASE_URL')) {
+ $baseUrl = getenv('MAGENTO_BASE_URL');
+ }
+
+ if (!$baseUrl) {
+ $output->writeln('Warning: Base URL not found. Please set MAGENTO_BASE_URL in your .env file.');
+ $baseUrl = "http://localhost"; // fallback
+ }
+
+ $output->writeln("Environment: " . (defined('TEST_ENV') ? TEST_ENV : (getenv('TEST_ENV') ?: 'local')));
+ $output->writeln("Base URL: " . $baseUrl);
+
+ // Get secrets from GitHub secrets (sensitive data)
+ $testSuiteId = getenv('TESTRIGOR_TEST_SUITE_ID') ?: getenv('MAGENTO_TEST_SUITE_ID');
+ $authToken = getenv('TESTRIGOR_AUTH_TOKEN') ?: getenv('MAGENTO_AUTH_TOKEN');
+
+ // Get configuration paths (non-sensitive, can be in repo or env vars)
+ $testCasesPath = getenv('TESTRIGOR_TEST_CASES_PATH') ?: getenv('TEST_CASES_PATH') ?: 'tests/testRigor/testcases';
+ $rulesPath = getenv('TESTRIGOR_RULES_PATH') ?: getenv('RULES_PATH') ?: 'tests/testRigor/rules';
+
+ $output->writeln("\nChecking GitHub secrets availability:");
+ $output->writeln("- Running in GitHub Actions: " . (getenv('GITHUB_ACTIONS') ? "Yes" : "No"));
+ $output->writeln("- GitHub Workspace: " . (getenv('GITHUB_WORKSPACE') ?: "Not set"));
+
+ // Check if required secrets are set (only sensitive data)
+ $missingSecrets = [];
+ if (!$testSuiteId) $missingSecrets[] = 'TESTRIGOR_TEST_SUITE_ID';
+ if (!$authToken) $missingSecrets[] = 'TESTRIGOR_AUTH_TOKEN';
+
+ if (!empty($missingSecrets)) {
+ $output->writeln("\nWarning: Missing required TestRigor secrets:");
+ $output->writeln("- TESTRIGOR_TEST_SUITE_ID: " . ($testSuiteId ? "Set" : "Missing"));
+ $output->writeln("- TESTRIGOR_AUTH_TOKEN: " . ($authToken ? "Set (hidden)" : "Missing"));
+
+ $output->writeln("\nConfiguration paths (using defaults if not set):");
+ $output->writeln("- Test Cases Path: " . $testCasesPath);
+ $output->writeln("- Rules Path: " . $rulesPath);
+
+ if (getenv('GITHUB_ACTIONS')) {
+ $output->writeln("\nTo fix this in GitHub Actions, add these secrets to your repository:");
+ $output->writeln("1. Go to your repository settings");
+ $output->writeln("2. Navigate to Secrets and variables → Actions");
+ $output->writeln("3. Add these repository secrets:");
+ $output->writeln(" - TESTRIGOR_TEST_SUITE_ID (your TestRigor application ID)");
+ $output->writeln(" - TESTRIGOR_AUTH_TOKEN (your TestRigor API token)");
+ $output->writeln("\n4. In your workflow file, set them as environment variables:");
+ $output->writeln(" env:");
+ $output->writeln(" TESTRIGOR_TEST_SUITE_ID: \${{ secrets.TESTRIGOR_TEST_SUITE_ID }}");
+ $output->writeln(" TESTRIGOR_AUTH_TOKEN: \${{ secrets.TESTRIGOR_AUTH_TOKEN }}");
+ $output->writeln(" # Optional: Override default paths if needed");
+ $output->writeln(" TESTRIGOR_TEST_CASES_PATH: tests/testRigor/testcases");
+ $output->writeln(" TESTRIGOR_RULES_PATH: tests/testRigor/rules");
+ } else {
+ $output->writeln("\nFor local testing, add these to your .env file:");
+ $output->writeln("TESTRIGOR_TEST_SUITE_ID=your_test_suite_id");
+ $output->writeln("TESTRIGOR_AUTH_TOKEN=your_auth_token");
+ $output->writeln("# Optional: Override default paths if needed");
+ $output->writeln("TESTRIGOR_TEST_CASES_PATH=tests/testRigor/testcases");
+ $output->writeln("TESTRIGOR_RULES_PATH=tests/testRigor/rules");
+ }
+
+ return 1; // Exit with error code
+ } else {
+ $output->writeln("\nAll required secrets are available");
+ $output->writeln("Configuration:");
+ $output->writeln("- Test Cases Path: " . $testCasesPath);
+ $output->writeln("- Rules Path: " . $rulesPath);
+
+ // Ensure TestRigor CLI is installed
+ $output->writeln("\nChecking TestRigor CLI installation...");
+ if (!$this->ensureTestRigorInstalled($output)) {
+ return 1; // Exit with error if installation failed
+ }
+
+ // Get the testrigor binary path
+ $testRigorBin = $this->getTestRigorBinary();
+ if (!$testRigorBin) {
+ $output->writeln("TestRigor CLI not found after installation.");
+ return 1;
+ }
+
+ // Build and execute the TestRigor command
+ $command = sprintf(
+ '%s test-suite run %s --token %s --url %s --test-cases-path %s --rules-path %s',
+ $testRigorBin,
+ escapeshellarg($testSuiteId),
+ escapeshellarg($authToken),
+ escapeshellarg($baseUrl),
+ escapeshellarg($testCasesPath),
+ escapeshellarg($rulesPath)
+ );
+
+ $output->writeln("\nExecuting TestRigor command:");
+ // Don't show the actual token in logs for security
+ $safeCommand = str_replace($authToken, '***HIDDEN***', $command);
+ $output->writeln($safeCommand);
+ $output->writeln("");
+
+ // Execute the command and capture output
+ $process = shell_exec($command . ' 2>&1');
+
+ if ($process) {
+ $output->writeln("TestRigor output:");
+ $output->writeln($process);
+ } else {
+ $output->writeln("No output from TestRigor command.");
+ }
+
+ $output->writeln("\nTestRigor integration completed successfully!");
+ return 0; // Success
+ }
+ }
+}
\ No newline at end of file