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