diff --git a/.gitignore b/.gitignore new file mode 100755 index 000000000..8e2330435 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +composer.phar +vendor/* +.env +_generated +AcceptanceTester.php diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..4aeda749c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: php +php: + - 7.0 + - 7.1 +install: composer install --no-interaction --prefer-source +script: + - vendor/bin/phpcs ./src --standard=./dev/tests/static/Magento diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 9efc40690..f93325c42 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# magento2-functional-testing-framework \ No newline at end of file +# Magento2 Functional Testing Framework + +Customized codeception modules, helpers, page objects and step objects for Magento 2.2.x. This library package can be used as dependency for Magento Acceptance Test projects for Magento 2 Community Edition ([magento/acceptance-test-ce](https://github.com/magento-pangolin/acceptance-test-ce/)) or Magento 2 Enterprise Edition in ([magento/acceptance-test-ee](https://github.com/magento-pangolin/acceptance-test-ee/)) or acceptance test projects for Magento 2 extensions. + +## Installation +Add the package into your acceptance test project composer.json: +``` + { + "require": { + "magento/acceptance-test-framework": "dev-develop" + } + } +``` +Then run: +``` + composer update +``` diff --git a/composer.json b/composer.json new file mode 100755 index 000000000..2bd7d5aea --- /dev/null +++ b/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/magento2-functional-testing-framework", + "type": "library", + "description": "Magento2 Functional Testing Framework", + "keywords": ["magento", "automation", "functional", "testing"], + "require": { + "php": "~7.0", + "codeception/codeception": "2.2|2.3", + "flow/jsonpath": ">0.2", + "fzaninotto/faker": "^1.6" + }, + "require-dev": { + "squizlabs/php_codesniffer": "1.5.3", + "sebastian/phpcpd": "~3.0" + }, + "autoload": { + "psr-4": { + "Magento\\FunctionalTestingFramework\\": ["src/Magento/FunctionalTestingFramework"] + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 000000000..88f87805a --- /dev/null +++ b/composer.lock @@ -0,0 +1,2668 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "4ccd9ed4f8c2fde43e5784bdc02b4064", + "content-hash": "d2c73e722ab7776b4cf486b17e2abfa7", + "packages": [ + { + "name": "behat/gherkin", + "version": "v4.4.5", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.5|~5", + "symfony/phpunit-bridge": "~2.7|~3", + "symfony/yaml": "~2.3|~3" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "time": "2016-10-30 11:50:56" + }, + { + "name": "codeception/codeception", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/Codeception/Codeception.git", + "reference": "b54eaf4007484f36145c1dc8c64da1874adbc340" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/b54eaf4007484f36145c1dc8c64da1874adbc340", + "reference": "b54eaf4007484f36145c1dc8c64da1874adbc340", + "shasum": "" + }, + "require": { + "behat/gherkin": "~4.4.0", + "ext-json": "*", + "ext-mbstring": "*", + "facebook/webdriver": ">=1.0.1 <2.0", + "guzzlehttp/guzzle": ">=4.1.4 <7.0", + "guzzlehttp/psr7": "~1.0", + "php": ">=5.4.0 <8.0", + "phpunit/php-code-coverage": ">=2.2.4 <6.0", + "phpunit/phpunit": ">4.8.20 <6.0", + "phpunit/phpunit-mock-objects": ">2.3 <5.0", + "sebastian/comparator": ">1.1 <3.0", + "sebastian/diff": "^1.4", + "stecman/symfony-console-completion": "^0.7.0", + "symfony/browser-kit": ">=2.7 <4.0", + "symfony/console": ">=2.7 <4.0", + "symfony/css-selector": ">=2.7 <4.0", + "symfony/dom-crawler": ">=2.7.5 <4.0", + "symfony/event-dispatcher": ">=2.7 <4.0", + "symfony/finder": ">=2.7 <4.0", + "symfony/yaml": ">=2.7 <4.0" + }, + "require-dev": { + "codeception/specify": "~0.3", + "facebook/graph-sdk": "~5.3", + "flow/jsonpath": "~0.2", + "league/factory-muffin": "^3.0", + "league/factory-muffin-faker": "^1.0", + "mongodb/mongodb": "^1.0", + "monolog/monolog": "~1.8", + "pda/pheanstalk": "~3.0", + "php-amqplib/php-amqplib": "~2.4", + "predis/predis": "^1.0", + "squizlabs/php_codesniffer": "~2.0", + "vlucas/phpdotenv": "^2.4.0" + }, + "suggest": { + "codeception/specify": "BDD-style code blocks", + "codeception/verify": "BDD-style assertions", + "flow/jsonpath": "For using JSONPath in REST module", + "league/factory-muffin": "For DataFactory module", + "league/factory-muffin-faker": "For Faker support in DataFactory module", + "phpseclib/phpseclib": "for SFTP option in FTP Module", + "symfony/phpunit-bridge": "For phpunit-bridge support" + }, + "bin": [ + "codecept" + ], + "type": "library", + "extra": { + "branch-alias": [] + }, + "autoload": { + "psr-4": { + "Codeception\\": "src\\Codeception", + "Codeception\\Extension\\": "ext" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Bodnarchuk", + "email": "davert@mail.ua", + "homepage": "http://codegyre.com" + } + ], + "description": "BDD-style testing framework", + "homepage": "http://codeception.com/", + "keywords": [ + "BDD", + "TDD", + "acceptance testing", + "functional testing", + "unit testing" + ], + "time": "2017-05-22 23:47:35" + }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "facebook/webdriver", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/facebook/php-webdriver.git", + "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/eadb0b7a7c3e6578185197fd40158b08c3164c83", + "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-zip": "*", + "php": "^5.5 || ~7.0", + "symfony/process": "^2.8 || ^3.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "php-mock/php-mock-phpunit": "^1.1", + "phpunit/phpunit": "4.6.* || ~5.0", + "satooshi/php-coveralls": "^1.0", + "squizlabs/php_codesniffer": "^2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-community": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A PHP client for Selenium WebDriver", + "homepage": "https://github.com/facebook/php-webdriver", + "keywords": [ + "facebook", + "php", + "selenium", + "webdriver" + ], + "time": "2017-04-28 14:54:49" + }, + { + "name": "flow/jsonpath", + "version": "0.3.4", + "source": { + "type": "git", + "url": "https://github.com/FlowCommunications/JSONPath.git", + "reference": "00aa9c361e4d0a210dd95f3c917a1e0dde3a957f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FlowCommunications/JSONPath/zipball/00aa9c361e4d0a210dd95f3c917a1e0dde3a957f", + "reference": "00aa9c361e4d0a210dd95f3c917a1e0dde3a957f", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "peekmo/jsonpath": "dev-master", + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Flow\\JSONPath": "src/", + "Flow\\JSONPath\\Test": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stephen Frank", + "email": "stephen@flowsa.com" + } + ], + "description": "JSONPath implementation for parsing, searching and flattening arrays", + "time": "2016-09-06 17:43:18" + }, + { + "name": "fzaninotto/faker", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", + "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "ext-intl": "*", + "phpunit/phpunit": "^4.0 || ^5.0", + "squizlabs/php_codesniffer": "^1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "time": "2017-08-15 16:48:10" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0 || ^5.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2017-06-22 18:50:49" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20 10:07:11" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-03-20 17:10:46" + }, + { + "name": "myclabs/deep-copy", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2017-04-12 18:52:22" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2d3d238c433cf69caeb4842e97a3223a116f94b2", + "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2017-08-30 18:51:59" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-07-14 14:27:02" + }, + { + "name": "phpspec/prophecy", + "version": "v1.7.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2017-09-04 11:05:03" + }, + { + "name": "phpunit/php-code-coverage", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "^1.3", + "phpunit/php-text-template": "^1.2", + "phpunit/php-token-stream": "^1.4.2 || ^2.0", + "sebastian/code-unit-reverse-lookup": "^1.0", + "sebastian/environment": "^1.3.2 || ^2.0", + "sebastian/version": "^1.0 || ^2.0" + }, + "require-dev": { + "ext-xdebug": "^2.1.4", + "phpunit/phpunit": "^5.7" + }, + "suggest": { + "ext-xdebug": "^2.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2017-04-02 07:44:40" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03 07:40:28" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26 11:10:40" + }, + { + "name": "phpunit/php-token-stream", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0", + "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2017-08-20 05:47:52" + }, + { + "name": "phpunit/phpunit", + "version": "5.7.21", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "3b91adfb64264ddec5a2dee9851f354aa66327db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3b91adfb64264ddec5a2dee9851f354aa66327db", + "reference": "3b91adfb64264ddec5a2dee9851f354aa66327db", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.6.2", + "phpunit/php-code-coverage": "^4.0.4", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "^1.2.4", + "sebastian/diff": "^1.4.3", + "sebastian/environment": "^1.3.4 || ^2.0", + "sebastian/exporter": "~2.0", + "sebastian/global-state": "^1.1", + "sebastian/object-enumerator": "~2.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0.3|~2.0", + "symfony/yaml": "~2.1|~3.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2017-06-21 08:11:54" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2 || ^2.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2017-06-30 09:13:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06 14:39:51" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10 12:19:37" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04 06:30:41" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-01-29 09:50:25" + }, + { + "name": "sebastian/diff", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-05-22 07:24:03" + }, + { + "name": "sebastian/environment", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-11-26 07:53:53" + }, + { + "name": "sebastian/exporter", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-11-19 08:54:04" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/object-enumerator", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-02-18 15:18:39" + }, + { + "name": "sebastian/recursion-context", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2016-11-19 07:33:16" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28 20:34:47" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03 07:35:21" + }, + { + "name": "stecman/symfony-console-completion", + "version": "0.7.0", + "source": { + "type": "git", + "url": "https://github.com/stecman/symfony-console-completion.git", + "reference": "5461d43e53092b3d3b9dbd9d999f2054730f4bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stecman/symfony-console-completion/zipball/5461d43e53092b3d3b9dbd9d999f2054730f4bbb", + "reference": "5461d43e53092b3d3b9dbd9d999f2054730f4bbb", + "shasum": "" + }, + "require": { + "php": ">=5.3.2", + "symfony/console": "~2.3 || ~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Stecman\\Component\\Symfony\\Console\\BashCompletion\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stephen Holdaway", + "email": "stephen@stecman.co.nz" + } + ], + "description": "Automatic BASH completion for Symfony Console Component based applications.", + "time": "2016-02-24 05:08:54" + }, + { + "name": "symfony/browser-kit", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "aee7120b058c268363e606ff5fe8271da849a1b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/aee7120b058c268363e606ff5fe8271da849a1b5", + "reference": "aee7120b058c268363e606ff5fe8271da849a1b5", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/dom-crawler": "~2.8|~3.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "time": "2017-07-29 21:54:42" + }, + { + "name": "symfony/console", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d6596cb5022b6a0bd940eae54a1de78646a5fda6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d6596cb5022b6a0bd940eae54a1de78646a5fda6", + "reference": "d6596cb5022b6a0bd940eae54a1de78646a5fda6", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3", + "symfony/dependency-injection": "~3.3", + "symfony/event-dispatcher": "~2.8|~3.0", + "symfony/filesystem": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/filesystem": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2017-08-27 14:52:21" + }, + { + "name": "symfony/css-selector", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "c5f5263ed231f164c58368efbce959137c7d9488" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/c5f5263ed231f164c58368efbce959137c7d9488", + "reference": "c5f5263ed231f164c58368efbce959137c7d9488", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2017-07-29 21:54:42" + }, + { + "name": "symfony/debug", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "084d804fe35808eb2ef596ec83d85d9768aa6c9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/084d804fe35808eb2ef596ec83d85d9768aa6c9d", + "reference": "084d804fe35808eb2ef596ec83d85d9768aa6c9d", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/http-kernel": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2017-08-27 14:52:21" + }, + { + "name": "symfony/dom-crawler", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "d15dfaf71b65bf3affb80900470caf4451a8217e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d15dfaf71b65bf3affb80900470caf4451a8217e", + "reference": "d15dfaf71b65bf3affb80900470caf4451a8217e", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "time": "2017-08-15 13:31:09" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "54ca9520a00386f83bca145819ad3b619aaa2485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/54ca9520a00386f83bca145819ad3b619aaa2485", + "reference": "54ca9520a00386f83bca145819ad3b619aaa2485", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", + "symfony/expression-language": "~2.8|~3.0", + "symfony/stopwatch": "~2.8|~3.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2017-07-29 21:54:42" + }, + { + "name": "symfony/finder", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/b2260dbc80f3c4198f903215f91a1ac7fe9fe09e", + "reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2017-07-29 21:54:42" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803", + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2017-06-14 15:44:48" + }, + { + "name": "symfony/process", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0", + "reference": "b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2017-07-29 21:54:42" + }, + { + "name": "symfony/yaml", + "version": "v3.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1d8c2a99c80862bdc3af94c1781bf70f86bccac0", + "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-07-29 21:54:42" + }, + { + "name": "webmozart/assert", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23 20:04:58" + } + ], + "packages-dev": [ + { + "name": "sebastian/finder-facade", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/finder-facade.git", + "reference": "2a6f7f57efc0aa2d23297d9fd9e2a03111a8c0b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/2a6f7f57efc0aa2d23297d9fd9e2a03111a8c0b9", + "reference": "2a6f7f57efc0aa2d23297d9fd9e2a03111a8c0b9", + "shasum": "" + }, + "require": { + "symfony/finder": "~2.3|~3.0", + "theseer/fdomdocument": "~1.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.", + "homepage": "https://github.com/sebastianbergmann/finder-facade", + "time": "2016-02-17 07:02:23" + }, + { + "name": "sebastian/phpcpd", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpcpd.git", + "reference": "d7006078b75a34c9250831c3453a2e256a687615" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/d7006078b75a34c9250831c3453a2e256a687615", + "reference": "d7006078b75a34c9250831c3453a2e256a687615", + "shasum": "" + }, + "require": { + "php": "^5.6|^7.0", + "phpunit/php-timer": "^1.0.6", + "sebastian/finder-facade": "^1.1", + "sebastian/version": "^2.0", + "symfony/console": "^3.0" + }, + "bin": [ + "phpcpd" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Copy/Paste Detector (CPD) for PHP code.", + "homepage": "https://github.com/sebastianbergmann/phpcpd", + "time": "2017-02-05 07:48:01" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "396178ada8499ec492363587f037125bf7b07fcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/396178ada8499ec492363587f037125bf7b07fcc", + "reference": "396178ada8499ec492363587f037125bf7b07fcc", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.1.2" + }, + "suggest": { + "phpunit/php-timer": "dev-master" + }, + "bin": [ + "scripts/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-phpcs-fixer": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "CodeSniffer.php", + "CodeSniffer/CLI.php", + "CodeSniffer/Exception.php", + "CodeSniffer/File.php", + "CodeSniffer/Report.php", + "CodeSniffer/Reporting.php", + "CodeSniffer/Sniff.php", + "CodeSniffer/Tokens.php", + "CodeSniffer/Reports/", + "CodeSniffer/CommentParser/", + "CodeSniffer/Tokenizers/", + "CodeSniffer/DocGenerators/", + "CodeSniffer/Standards/AbstractPatternSniff.php", + "CodeSniffer/Standards/AbstractScopeSniff.php", + "CodeSniffer/Standards/AbstractVariableSniff.php", + "CodeSniffer/Standards/IncorrectPatternException.php", + "CodeSniffer/Standards/Generic/Sniffs/", + "CodeSniffer/Standards/MySource/Sniffs/", + "CodeSniffer/Standards/PEAR/Sniffs/", + "CodeSniffer/Standards/PSR1/Sniffs/", + "CodeSniffer/Standards/PSR2/Sniffs/", + "CodeSniffer/Standards/Squiz/Sniffs/", + "CodeSniffer/Standards/Zend/Sniffs/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenises PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2014-05-01 03:07:07" + }, + { + "name": "theseer/fdomdocument", + "version": "1.6.6", + "source": { + "type": "git", + "url": "https://github.com/theseer/fDOMDocument.git", + "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca", + "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "lib-libxml": "*", + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "lead" + } + ], + "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", + "homepage": "https://github.com/theseer/fDOMDocument", + "time": "2017-06-30 11:53:12" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "~7.0" + }, + "platform-dev": [] +} diff --git a/dev/tests/static/Magento/Sniffs/Annotations/Helper.php b/dev/tests/static/Magento/Sniffs/Annotations/Helper.php new file mode 100644 index 000000000..53a03b574 --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Annotations/Helper.php @@ -0,0 +1,576 @@ + + * @author Marc McIntyre + * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @link http://pear.php.net/package/PHP_CodeSniffer + * + * @SuppressWarnings(PHPMD) + */ +class Helper +{ + const ERROR_PARSING = 'ErrorParsing'; + + const AMBIGUOUS_TYPE = 'AmbiguousType'; + + const MISSING = 'Missing'; + + const WRONG_STYLE = 'WrongStyle'; + + const WRONG_END = 'WrongEnd'; + + const FAILED_PARSE = 'FailedParse'; + + const CONTENT_AFTER_OPEN = 'ContentAfterOpen'; + + const MISSING_SHORT = 'MissingShort'; + + const EMPTY_DOC = 'Empty'; + + const SPACING_BETWEEN = 'SpacingBetween'; + + const SPACING_BEFORE_SHORT = 'SpacingBeforeShort'; + + const SPACING_BEFORE_TAGS = 'SpacingBeforeTags'; + + const SHORT_SINGLE_LINE = 'ShortSingleLine'; + + const SHORT_NOT_CAPITAL = 'ShortNotCapital'; + + const SHORT_FULL_STOP = 'ShortFullStop'; + + const SPACING_AFTER = 'SpacingAfter'; + + const SEE_ORDER = 'SeeOrder'; + + const EMPTY_SEE = 'EmptySee'; + + const SEE_INDENT = 'SeeIndent'; + + const DUPLICATE_RETURN = 'DuplicateReturn'; + + const MISSING_PARAM_TAG = 'MissingParamTag'; + + const SPACING_AFTER_LONG_NAME = 'SpacingAfterLongName'; + + const SPACING_AFTER_LONG_TYPE = 'SpacingAfterLongType'; + + const MISSING_PARAM_TYPE = 'MissingParamType'; + + const MISSING_PARAM_NAME = 'MissingParamName'; + + const EXTRA_PARAM_COMMENT = 'ExtraParamComment'; + + const PARAM_NAME_NO_MATCH = 'ParamNameNoMatch'; + + const PARAM_NAME_NO_CASE_MATCH = 'ParamNameNoCaseMatch'; + + const INVALID_TYPE_HINT = 'InvalidTypeHint'; + + const INCORRECT_TYPE_HINT = 'IncorrectTypeHint'; + + const TYPE_HINT_MISSING = 'TypeHintMissing'; + + const INCORRECT_PARAM_VAR_NAME = 'IncorrectParamVarName'; + + const RETURN_ORDER = 'ReturnOrder'; + + const MISSING_RETURN_TYPE = 'MissingReturnType'; + + const INVALID_RETURN = 'InvalidReturn'; + + const INVALID_RETURN_VOID = 'InvalidReturnVoid'; + + const INVALID_NO_RETURN = 'InvalidNoReturn'; + + const INVALID_RETURN_NOT_VOID = 'InvalidReturnNotVoid'; + + const INCORRECT_INHERIT_DOC = 'IncorrectInheritDoc'; + + const RETURN_INDENT = 'ReturnIndent'; + + const MISSING_RETURN = 'MissingReturn'; + + const RETURN_NOT_REQUIRED = 'ReturnNotRequired'; + + const INVALID_THROWS = 'InvalidThrows'; + + const THROWS_NOT_CAPITAL = 'ThrowsNotCapital'; + + const THROWS_ORDER = 'ThrowsOrder'; + + const EMPTY_THROWS = 'EmptyThrows'; + + const THROWS_NO_FULL_STOP = 'ThrowsNoFullStop'; + + const SPACING_AFTER_PARAMS = 'SpacingAfterParams'; + + const SPACING_BEFORE_PARAMS = 'SpacingBeforeParams'; + + const SPACING_BEFORE_PARAM_TYPE = 'SpacingBeforeParamType'; + + const LONG_NOT_CAPITAL = 'LongNotCapital'; + + const TAG_NOT_ALLOWED = 'TagNotAllowed'; + + const DUPLICATE_VAR = 'DuplicateVar'; + + const VAR_ORDER = 'VarOrder'; + + const MISSING_VAR_TYPE = 'MissingVarType'; + + const INCORRECT_VAR_TYPE = 'IncorrectVarType'; + + const VAR_INDENT = 'VarIndent'; + + const MISSING_VAR = 'MissingVar'; + + const MISSING_PARAM_COMMENT = 'MissingParamComment'; + + const PARAM_COMMENT_NOT_CAPITAL = 'ParamCommentNotCapital'; + + const PARAM_COMMENT_FULL_STOP = 'ParamCommentFullStop'; + + // tells phpcs to use the default level + const ERROR = 0; + + // default level of warnings is 5 + const WARNING = 6; + + const INFO = 2; + + // Lowest possible level. + const OFF = 1; + + const LEVEL = 'level'; + + const MESSAGE = 'message'; + + /** + * Map of Error Type to Error Severity + * + * @var array + */ + protected static $reportingLevel = [ + self::ERROR_PARSING => [self::LEVEL => self::ERROR, self::MESSAGE => '%s'], + self::FAILED_PARSE => [self::LEVEL => self::ERROR, self::MESSAGE => '%s'], + self::AMBIGUOUS_TYPE => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Ambiguous type "%s" for %s is NOT recommended', + ], + self::MISSING => [self::LEVEL => self::ERROR, self::MESSAGE => 'Missing %s doc comment'], + self::WRONG_STYLE => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'You must use "/**" style comments for a %s comment', + ], + self::WRONG_END => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'You must use "*/" to end a function comment; found "%s"', + ], + self::EMPTY_DOC => [self::LEVEL => self::WARNING, self::MESSAGE => '%s doc comment is empty'], + self::CONTENT_AFTER_OPEN => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'The open comment tag must be the only content on the line', + ], + self::MISSING_SHORT => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Missing short description in %s doc comment', + ], + self::SPACING_BETWEEN => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'There must be exactly one blank line between descriptions in %s comment', + ], + self::SPACING_BEFORE_SHORT => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Extra newline(s) found before %s comment short description', + ], + self::SPACING_BEFORE_TAGS => [ + self::LEVEL => self::INFO, + self::MESSAGE => 'There must be exactly one blank line before the tags in %s comment', + ], + self::SHORT_SINGLE_LINE => [ + self::LEVEL => self::OFF, + self::MESSAGE => '%s comment short description must be on a single line', + ], + self::SHORT_NOT_CAPITAL => [ + self::LEVEL => self::WARNING, + self::MESSAGE => '%s comment short description must start with a capital letter', + ], + self::SHORT_FULL_STOP => [ + self::LEVEL => self::OFF, + self::MESSAGE => '%s comment short description must end with a full stop', + ], + self::SPACING_AFTER => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Additional blank lines found at end of %s comment', + ], + self::SEE_ORDER => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'The @see tag is in the wrong order; the tag precedes @return', + ], + self::EMPTY_SEE => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Content missing for @see tag in %s comment', + ], + self::SEE_INDENT => [ + self::LEVEL => self::OFF, + self::MESSAGE => '@see tag indented incorrectly; expected 1 spaces but found %s', + ], + self::DUPLICATE_RETURN => [ + self::LEVEL => self::ERROR, + self::MESSAGE => 'Only 1 @return tag is allowed in function comment', + ], + self::MISSING_PARAM_TAG => [self::LEVEL => self::ERROR, self::MESSAGE => 'Doc comment for "%s" missing'], + self::SPACING_AFTER_LONG_NAME => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'Expected 1 space after the longest variable name', + ], + self::SPACING_AFTER_LONG_TYPE => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'Expected 1 space after the longest type', + ], + self::MISSING_PARAM_TYPE => [self::LEVEL => self::ERROR, self::MESSAGE => 'Missing type at position %s'], + self::MISSING_PARAM_NAME => [ + self::LEVEL => self::ERROR, + self::MESSAGE => 'Missing parameter name at position %s', + ], + self::EXTRA_PARAM_COMMENT => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Superfluous doc comment at position %s', + ], + self::PARAM_NAME_NO_MATCH => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Doc comment for var %s does not match actual variable name %s at position %s', + ], + self::PARAM_NAME_NO_CASE_MATCH => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Doc comment for var %s does not match case of actual variable name %s at position %s', + ], + self::INVALID_TYPE_HINT => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Unknown type hint "%s" found for %s at position %s', + ], + self::INCORRECT_TYPE_HINT => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Expected type hint "%s"; found "%s" for %s at position %s', + ], + self::TYPE_HINT_MISSING => [ + self::LEVEL => self::INFO, + self::MESSAGE => 'Type hint "%s" missing for %s at position %s', + ], + self::INCORRECT_PARAM_VAR_NAME => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Expected "%s"; found "%s" for %s at position %s', + ], + self::RETURN_ORDER => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'The @return tag is in the wrong order; the tag follows @see (if used)', + ], + self::MISSING_RETURN_TYPE => [ + self::LEVEL => self::ERROR, + self::MESSAGE => 'Return type missing for @return tag in function comment', + ], + self::INVALID_RETURN => [ + self::LEVEL => self::ERROR, + self::MESSAGE => 'Function return type "%s" is invalid', + ], + self::INVALID_RETURN_VOID => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Function return type is void, but function contains return statement', + ], + self::INVALID_NO_RETURN => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Function return type is not void, but function has no return statement', + ], + self::INVALID_RETURN_NOT_VOID => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Function return type is not void, but function is returning void here', + ], + self::INCORRECT_INHERIT_DOC => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'The incorrect inherit doc tag usage. Should be {@inheritdoc}', + ], + self::RETURN_INDENT => [ + self::LEVEL => self::OFF, + self::MESSAGE => '@return tag indented incorrectly; expected 1 space but found %s', + ], + self::MISSING_RETURN => [ + self::LEVEL => self::ERROR, + self::MESSAGE => 'Missing @return tag in function comment', + ], + self::RETURN_NOT_REQUIRED => [ + self::LEVEL => self::WARNING, + self::MESSAGE => '@return tag is not required for constructor and destructor', + ], + self::INVALID_THROWS => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Exception type and comment missing for @throws tag in function comment', + ], + self::THROWS_NOT_CAPITAL => [ + self::LEVEL => self::WARNING, + self::MESSAGE => '@throws tag comment must start with a capital letter', + ], + self::THROWS_ORDER => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'The @throws tag is in the wrong order; the tag follows @return', + ], + self::EMPTY_THROWS => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'Comment missing for @throws tag in function comment', + ], + self::THROWS_NO_FULL_STOP => [ + self::LEVEL => self::OFF, + self::MESSAGE => '@throws tag comment must end with a full stop', + ], + self::SPACING_AFTER_PARAMS => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'Last parameter comment requires a blank newline after it', + ], + self::SPACING_BEFORE_PARAMS => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Parameters must appear immediately after the comment', + ], + self::SPACING_BEFORE_PARAM_TYPE => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'Expected 1 space before variable type', + ], + self::LONG_NOT_CAPITAL => [ + self::LEVEL => self::WARNING, + self::MESSAGE => '%s comment long description must start with a capital letter', + ], + self::TAG_NOT_ALLOWED => [ + self::LEVEL => self::WARNING, + self::MESSAGE => '@%s tag is not allowed in variable comment', + ], + self::DUPLICATE_VAR => [ + self::LEVEL => self::ERROR, + self::MESSAGE => 'Only 1 @var tag is allowed in variable comment', + ], + self::VAR_ORDER => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'The @var tag must be the first tag in a variable comment', + ], + self::MISSING_VAR_TYPE => [ + self::LEVEL => self::ERROR, + self::MESSAGE => 'Var type missing for @var tag in variable comment', + ], + self::INCORRECT_VAR_TYPE => [ + self::LEVEL => self::ERROR, + self::MESSAGE => 'Expected "%s"; found "%s" for @var tag in variable comment', + ], + self::VAR_INDENT => [ + self::LEVEL => self::OFF, + self::MESSAGE => '@var tag indented incorrectly; expected 1 space but found %s', + ], + self::MISSING_VAR => [ + self::LEVEL => self::WARNING, + self::MESSAGE => 'Missing @var tag in variable comment', + ], + self::MISSING_PARAM_COMMENT => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'Missing comment for param "%s" at position %s', + ], + self::PARAM_COMMENT_NOT_CAPITAL => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'Param comment must start with a capital letter', + ], + self::PARAM_COMMENT_FULL_STOP => [ + self::LEVEL => self::OFF, + self::MESSAGE => 'Param comment must end with a full stop', + ], + ]; + + /** + * List of allowed types + * + * @var string[] + */ + protected static $allowedTypes = [ + 'array', + 'boolean', + 'bool', + 'float', + 'integer', + 'int', + 'object', + 'string', + 'resource', + 'callable', + 'true', + 'false', + ]; + + /** + * The current PHP_CodeSniffer_File object we are processing. + * + * @var PHP_CodeSniffer_File + */ + protected $currentFile = null; + + /** + * Constructor for class. + * + * @param PHP_CodeSniffer_File $phpcsFile + */ + public function __construct(PHP_CodeSniffer_File $phpcsFile) + { + $this->currentFile = $phpcsFile; + } + + /** + * Returns the current file object + * + * @return PHP_CodeSniffer_File + */ + public function getCurrentFile() + { + return $this->currentFile; + } + + /** + * Returns the eol character used in the file + * + * @return string + */ + public function getEolChar() + { + return $this->currentFile->eolChar; + } + + /** + * Returns the array of allowed types for magento standard + * + * @return string[] + */ + public function getAllowedTypes() + { + return self::$allowedTypes; + } + + /** + * This method will add the message as an error or warning depending on the configuration + * + * @param int $stackPtr The stack position where the error occurred. + * @param string $code A violation code unique to the sniff message. + * @param string[] $data Replacements for the error message. + * @param int $severity The severity level for this error. A value of 0 + * @return void + */ + public function addMessage($stackPtr, $code, $data = [], $severity = 0) + { + // Does the $code key exist in the report level + if (array_key_exists($code, self::$reportingLevel)) { + $message = self::$reportingLevel[$code][self::MESSAGE]; + $level = self::$reportingLevel[$code][self::LEVEL]; + if ($level === self::WARNING || $level === self::INFO || $level === self::OFF) { + $s = $level; + if ($severity !== 0) { + $s = $severity; + } + $this->currentFile->addWarning($message, $stackPtr, $code, $data, $s); + } else { + $this->currentFile->addError($message, $stackPtr, $code, $data, $severity); + } + } + } + + /** + * Returns if we should filter a particular file + * + * @return bool + */ + public function shouldFilter() + { + $shouldFilter = false; + $filename = $this->getCurrentFile()->getFilename(); + if (preg_match('#(?:/|\\\\)dev(?:/|\\\\)tests(?:/|\\\\)#', $filename)) { + // TODO: Temporarily blacklist anything in dev/tests until a sweep of dev/tests can be made. + // This block of the if should be removed leaving only the phtml condition when dev/tests is swept. + // Skip all dev tests files + $shouldFilter = true; + } elseif (preg_match('#(?:/|\\\\)Test(?:/|\\\\)Unit(?:/|\\\\)#', $filename)) { + $shouldFilter = true; + } elseif (preg_match('/\\.phtml$/', $filename)) { + // Skip all phtml files + $shouldFilter = true; + } + + return $shouldFilter; + } + + /** + * Determine if text is a class name + * + * @param string $class + * @return bool + */ + protected function isClassName($class) + { + $return = false; + if (preg_match('/^\\\\?[A-Z]\\w+(?:\\\\\\w+)*?$/', $class)) { + $return = true; + } + return $return; + } + + /** + * Determine if the text has an ambiguous type + * + * @param string $text + * @param array &$matches Type that was detected as ambiguous is in result. + * @return bool + */ + public function isAmbiguous($text, &$matches = []) + { + return preg_match('/(mixed)/', $text, $matches); + } + + /** + * Take the type and suggest the correct one. + * + * @param string $type + * @return string + */ + public function suggestType($type) + { + $suggestedName = null; + // First check to see if this type is a list of types. If so we break it up and check each + if (preg_match('/^.*?(?:\|.*)+$/', $type)) { + // Return list of all types in this string. + $types = explode('|', $type); + if (is_array($types)) { + // Loop over all types and call this method on each. + $suggestions = []; + foreach ($types as $t) { + $suggestions[] = $this->suggestType($t); + } + // Now that we have suggestions put them back together. + $suggestedName = implode('|', $suggestions); + } else { + $suggestedName = 'Unknown'; + } + } elseif ($this->isClassName($type)) { + // If this looks like a class name. + $suggestedName = $type; + } else { + // Only one type First check if that type is a base one. + $lowerVarType = strtolower($type); + if (in_array($lowerVarType, self::$allowedTypes)) { + $suggestedName = $lowerVarType; + } + // If no name suggested yet then call the phpcs version of this method. + if (empty($suggestedName)) { + $suggestedName = PHP_CodeSniffer::suggestType($type); + } + } + return $suggestedName; + } +} diff --git a/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedAttributesSniff.php b/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedAttributesSniff.php new file mode 100644 index 000000000..98fc99a0e --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedAttributesSniff.php @@ -0,0 +1,357 @@ + + *
  • A variable doc comment exists.
  • + *
  • Short description ends with a full stop.
  • + *
  • There is a blank line after the short description.
  • + *
  • There is a blank line between the description and the tags.
  • + *
  • Check the order, indentation and content of each tag.
  • + * + * + * @author Greg Sherwood + * @author Marc McIntyre + * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @version Release: @package_version@ + * @link http://pear.php.net/package/PHP_CodeSniffer + * + * @SuppressWarnings(PHPMD) + */ +class RequireAnnotatedAttributesSniff extends PHP_CodeSniffer_Standards_AbstractVariableSniff +{ + /** + * The header comment parser for the current file. + * + * @var PHP_CodeSniffer_CommentParser_ClassCommentParser + */ + protected $commentParser = null; + + /** + * The sniff helper for stuff shared between the annotations sniffs + * + * @var Helper + */ + protected $helper = null; + + /** + * Extract the var comment docblock + * + * @param array $tokens + * @param string $commentToken + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @return int|false + */ + protected function extractVarDocBlock($tokens, $commentToken, $stackPtr) + { + $commentEnd = $this->helper->getCurrentFile()->findPrevious($commentToken, $stackPtr - 3); + $break = false; + if ($commentEnd !== false && $tokens[$commentEnd]['code'] === T_COMMENT) { + $this->helper->addMessage($stackPtr, Helper::WRONG_STYLE, ['variable']); + $break = true; + } elseif ($commentEnd === false || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT) { + $this->helper->addMessage($stackPtr, Helper::MISSING, ['variable']); + $break = true; + } else { + // Make sure the comment we have found belongs to us. + $commentFor = $this->helper->getCurrentFile()->findNext( + [T_VARIABLE, T_CLASS, T_INTERFACE], + $commentEnd + 1 + ); + if ($commentFor !== $stackPtr) { + $this->helper->addMessage($stackPtr, Helper::MISSING, ['variable']); + $break = true; + } + } + return $break ? false : $commentEnd; + } + + /** + * Checks for short and long descriptions on variable definitions + * + * @param PHP_CodeSniffer_CommentParser_CommentElement $comment + * @param int $commentStart + * @return void + */ + protected function checkForDescription($comment, $commentStart) + { + $short = $comment->getShortComment(); + $long = ''; + $newlineCount = 0; + if (trim($short) === '') { + $this->helper->addMessage($commentStart, Helper::MISSING_SHORT, ['variable']); + $newlineCount = 1; + } else { + // No extra newline before short description. + $newlineSpan = strspn($short, $this->helper->getEolChar()); + if ($short !== '' && $newlineSpan > 0) { + $this->helper->addMessage($commentStart + 1, Helper::SPACING_BEFORE_SHORT, ['variable']); + } + + $newlineCount = substr_count($short, $this->helper->getEolChar()) + 1; + + // Exactly one blank line between short and long description. + $long = $comment->getLongComment(); + if (empty($long) === false) { + $between = $comment->getWhiteSpaceBetween(); + $newlineBetween = substr_count($between, $this->helper->getEolChar()); + if ($newlineBetween !== 2) { + $this->helper->addMessage( + $commentStart + $newlineCount + 1, + Helper::SPACING_BETWEEN, + ['variable'] + ); + } + + $newlineCount += $newlineBetween; + + $testLong = trim($long); + if (preg_match('|\p{Lu}|u', $testLong[0]) === 0) { + $this->helper->addMessage( + $commentStart + $newlineCount, + Helper::LONG_NOT_CAPITAL, + ['Variable'] + ); + } + } + + // Short description must be single line and end with a full stop. + $testShort = trim($short); + $lastChar = $testShort[strlen($testShort) - 1]; + if (substr_count($testShort, $this->helper->getEolChar()) !== 0) { + $this->helper->addMessage($commentStart + 1, Helper::SHORT_SINGLE_LINE, ['Variable']); + } + + if (preg_match('|\p{Lu}|u', $testShort[0]) === 0) { + $this->helper->addMessage($commentStart + 1, Helper::SHORT_NOT_CAPITAL, ['Variable']); + } + + if ($lastChar !== '.') { + $this->helper->addMessage($commentStart + 1, Helper::SHORT_FULL_STOP, ['Variable']); + } + } + // Exactly one blank line before tags. + $tags = $this->commentParser->getTagOrders(); + if (count($tags) > 1) { + $newlineSpan = $comment->getNewlineAfter(); + if ($newlineSpan !== 2) { + if ($long !== '') { + $newlineCount += substr_count($long, $this->helper->getEolChar()) - $newlineSpan + 1; + } + + $this->helper->addMessage( + $commentStart + $newlineCount, + Helper::SPACING_BEFORE_TAGS, + ['variable'] + ); + $short = rtrim($short, $this->helper->getEolChar() . ' '); + } + } + } + + /** + * Called to process class member vars. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function processMemberVar(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + { + $this->helper = new Helper($phpcsFile); + // if we should skip this type we should do that + if ($this->helper->shouldFilter()) { + return; + } + $tokens = $phpcsFile->getTokens(); + $commentToken = [T_COMMENT, T_DOC_COMMENT]; + + // Extract the var comment docblock. + $commentEnd = $this->extractVarDocBlock($tokens, $commentToken, $stackPtr); + if ($commentEnd === false) { + return; + } + + $commentStart = $phpcsFile->findPrevious(T_DOC_COMMENT, $commentEnd - 1, null, true) + 1; + $commentString = $phpcsFile->getTokensAsString($commentStart, $commentEnd - $commentStart + 1); + + // Parse the header comment docblock. + try { + $this->commentParser = new PHP_CodeSniffer_CommentParser_MemberCommentParser($commentString, $phpcsFile); + $this->commentParser->parse(); + } catch (PHP_CodeSniffer_CommentParser_ParserException $e) { + $line = $e->getLineWithinComment() + $commentStart; + $data = [$e->getMessage()]; + $this->helper->addMessage($line, Helper::ERROR_PARSING, $data); + return; + } + + $comment = $this->commentParser->getComment(); + if (($comment === null) === true) { + $this->helper->addMessage($commentStart, Helper::EMPTY_DOC, ['Variable']); + return; + } + + // The first line of the comment should just be the /** code. + $eolPos = strpos($commentString, $phpcsFile->eolChar); + $firstLine = substr($commentString, 0, $eolPos); + if ($firstLine !== '/**') { + $this->helper->addMessage($commentStart, Helper::CONTENT_AFTER_OPEN); + } + + // Check for a comment description. + $this->checkForDescription($comment, $commentStart); + + // Check for unknown/deprecated tags. + $unknownTags = $this->commentParser->getUnknown(); + foreach ($unknownTags as $errorTag) { + // Unknown tags are not parsed, do not process further. + $data = [$errorTag['tag']]; + $this->helper->addMessage($commentStart + $errorTag['line'], Helper::TAG_NOT_ALLOWED, $data); + } + + // Check each tag. + $this->processVar($commentStart, $commentEnd); + $this->processSees($commentStart); + + // The last content should be a newline and the content before + // that should not be blank. If there is more blank space + // then they have additional blank lines at the end of the comment. + $words = $this->commentParser->getWords(); + $lastPos = count($words) - 1; + if (trim( + $words[$lastPos - 1] + ) !== '' || strpos( + $words[$lastPos - 1], + $this->currentFile->eolChar + ) === false || trim( + $words[$lastPos - 2] + ) === '' + ) { + $this->helper->addMessage($commentEnd, Helper::SPACING_AFTER, ['variable']); + } + } + + /** + * Process the var tag. + * + * @param int $commentStart The position in the stack where the comment started. + * @param int $commentEnd The position in the stack where the comment ended. + * + * @return void + */ + protected function processVar($commentStart, $commentEnd) + { + $var = $this->commentParser->getVar(); + + if ($var !== null) { + $errorPos = $commentStart + $var->getLine(); + $index = array_keys($this->commentParser->getTagOrders(), 'var'); + + if (count($index) > 1) { + $this->helper->addMessage($errorPos, Helper::DUPLICATE_VAR); + return; + } + + if ($index[0] !== 1) { + $this->helper->addMessage($errorPos, Helper::VAR_ORDER); + } + + $content = $var->getContent(); + if (empty($content) === true) { + $this->helper->addMessage($errorPos, Helper::MISSING_VAR_TYPE); + return; + } else { + $suggestedType = $this->helper->suggestType($content); + if ($content !== $suggestedType) { + $data = [$suggestedType, $content]; + $this->helper->addMessage($errorPos, Helper::INCORRECT_VAR_TYPE, $data); + } elseif ($this->helper->isAmbiguous($content, $matches)) { + // Warn about ambiguous types ie array or mixed + $data = [$matches[1], '@var']; + $this->helper->addMessage($errorPos, Helper::AMBIGUOUS_TYPE, $data); + } + } + + $spacing = substr_count($var->getWhitespaceBeforeContent(), ' '); + if ($spacing !== 1) { + $data = [$spacing]; + $this->helper->addMessage($errorPos, Helper::VAR_INDENT, $data); + } + } else { + $this->helper->addMessage($commentEnd, Helper::MISSING_VAR); + } + } + + /** + * Process the see tags. + * + * @param int $commentStart The position in the stack where the comment started. + * + * @return void + */ + protected function processSees($commentStart) + { + $sees = $this->commentParser->getSees(); + if (empty($sees) === false) { + foreach ($sees as $see) { + $errorPos = $commentStart + $see->getLine(); + $content = $see->getContent(); + if (empty($content) === true) { + $this->helper->addMessage($errorPos, Helper::EMPTY_SEE, ['variable']); + continue; + } + + $spacing = substr_count($see->getWhitespaceBeforeContent(), ' '); + if ($spacing !== 1) { + $data = [$spacing]; + $this->helper->addMessage($errorPos, Helper::SEE_INDENT, $data); + } + } + } + } + + /** + * Called to process a normal variable. + * + * Not required for this sniff. + * + * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this token was found. + * @param int $stackPtr The position where the double quoted + * string was found. + * + * @return void + */ + protected function processVariable(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + { + } + + /** + * Called to process variables found in double quoted strings. + * + * Not required for this sniff. + * + * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this token was found. + * @param int $stackPtr The position where the double quoted + * string was found. + * + * @return void + */ + protected function processVariableInString(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + { + } +} diff --git a/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedMethodsSniff.php b/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedMethodsSniff.php new file mode 100644 index 000000000..63dc220b6 --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedMethodsSniff.php @@ -0,0 +1,694 @@ + + * @author Marc McIntyre + * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @link http://pear.php.net/package/PHP_CodeSniffer + * + * @SuppressWarnings(PHPMD) + */ +class RequireAnnotatedMethodsSniff implements PHP_CodeSniffer_Sniff +{ + /** + * The name of the method that we are currently processing. + * + * @var string + */ + private $_methodName = ''; + + /** + * The position in the stack where the function token was found. + * + * @var int + */ + private $_functionToken = null; + + /** + * The position in the stack where the class token was found. + * + * @var int + */ + private $_classToken = null; + + /** + * The index of the current tag we are processing. + * + * @var int + */ + private $_tagIndex = 0; + + /** + * The function comment parser for the current method. + * + * @var PHP_CodeSniffer_CommentParser_FunctionCommentParser + */ + protected $commentParser = null; + + /** + * The sniff helper for stuff shared between the annotations sniffs + * + * @var Helper + */ + protected $helper = null; + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + return [T_FUNCTION]; + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + { + $this->helper = new Helper($phpcsFile); + // if we should skip this type we should do that + if ($this->helper->shouldFilter()) { + return; + } + + $tokens = $phpcsFile->getTokens(); + + $find = [T_COMMENT, T_DOC_COMMENT, T_CLASS, T_FUNCTION, T_OPEN_TAG]; + + $commentEnd = $phpcsFile->findPrevious($find, $stackPtr - 1); + + if ($commentEnd === false) { + return; + } + + // If the token that we found was a class or a function, then this + // function has no doc comment. + $code = $tokens[$commentEnd]['code']; + + if ($code === T_COMMENT) { + // The function might actually be missing a comment, and this last comment + // found is just commenting a bit of code on a line. So if it is not the + // only thing on the line, assume we found nothing. + $prevContent = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, $commentEnd); + if ($tokens[$commentEnd]['line'] === $tokens[$commentEnd]['line']) { + $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']); + } else { + $this->helper->addMessage($stackPtr, Helper::WRONG_STYLE, ['function']); + } + return; + } elseif ($code !== T_DOC_COMMENT) { + $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']); + return; + } elseif (trim($tokens[$commentEnd]['content']) !== '*/') { + $this->helper->addMessage($commentEnd, Helper::WRONG_END, [trim($tokens[$commentEnd]['content'])]); + return; + } + + // If there is any code between the function keyword and the doc block + // then the doc block is not for us. + $ignore = PHP_CodeSniffer_Tokens::$scopeModifiers; + $ignore[] = T_STATIC; + $ignore[] = T_WHITESPACE; + $ignore[] = T_ABSTRACT; + $ignore[] = T_FINAL; + $prevToken = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true); + if ($prevToken !== $commentEnd) { + $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']); + return; + } + + $this->_functionToken = $stackPtr; + + $this->_classToken = null; + foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) { + if ($condition === T_CLASS || $condition === T_INTERFACE) { + $this->_classToken = $condPtr; + break; + } + } + + // Find the first doc comment. + $commentStart = $phpcsFile->findPrevious(T_DOC_COMMENT, $commentEnd - 1, null, true) + 1; + $commentString = $phpcsFile->getTokensAsString($commentStart, $commentEnd - $commentStart + 1); + $this->_methodName = $phpcsFile->getDeclarationName($stackPtr); + + try { + $this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($commentString, $phpcsFile); + $this->commentParser->parse(); + } catch (PHP_CodeSniffer_CommentParser_ParserException $e) { + $line = $e->getLineWithinComment() + $commentStart; + $this->helper->addMessage($line, Helper::FAILED_PARSE, [$e->getMessage()]); + return; + } + + $comment = $this->commentParser->getComment(); + if (($comment === null) === true) { + $this->helper->addMessage($commentStart, Helper::EMPTY_DOC, ['Function']); + return; + } + + // The first line of the comment should just be the /** code. + $eolPos = strpos($commentString, $phpcsFile->eolChar); + $firstLine = substr($commentString, 0, $eolPos); + if ($firstLine !== '/**') { + $this->helper->addMessage($commentStart, Helper::CONTENT_AFTER_OPEN); + } + + // If the comment has an inherit doc note just move on + if (preg_match('/\{\@inheritdoc\}/', $commentString)) { + return; + } elseif (preg_match('/\{?\@?inherit[dD]oc\}?/', $commentString)) { + $this->helper->addMessage($commentStart, Helper::INCORRECT_INHERIT_DOC); + return; + } + + $this->processParams($commentStart, $commentEnd); + $this->processSees($commentStart); + $this->processReturn($commentStart, $commentEnd); + $this->processThrows($commentStart); + + // Check for a comment description. + $short = $comment->getShortComment(); + if (trim($short) === '') { + $this->helper->addMessage($commentStart, Helper::MISSING_SHORT, ['function']); + return; + } + + // No extra newline before short description. + $newlineCount = 0; + $newlineSpan = strspn($short, $phpcsFile->eolChar); + if ($short !== '' && $newlineSpan > 0) { + $this->helper->addMessage($commentStart + 1, Helper::SPACING_BEFORE_SHORT, ['function']); + } + + $newlineCount = substr_count($short, $phpcsFile->eolChar) + 1; + + // Exactly one blank line between short and long description. + $long = $comment->getLongComment(); + if (empty($long) === false) { + $between = $comment->getWhiteSpaceBetween(); + $newlineBetween = substr_count($between, $phpcsFile->eolChar); + if ($newlineBetween !== 2) { + $this->helper->addMessage( + $commentStart + $newlineCount + 1, + Helper::SPACING_BETWEEN, + ['function'] + ); + } + $newlineCount += $newlineBetween; + $testLong = trim($long); + if (preg_match('|\p{Lu}|u', $testLong[0]) === 0) { + $this->helper->addMessage($commentStart + $newlineCount, Helper::LONG_NOT_CAPITAL, ['Function']); + } + } + + // Exactly one blank line before tags. + $params = $this->commentParser->getTagOrders(); + if (count($params) > 1) { + $newlineSpan = $comment->getNewlineAfter(); + if ($newlineSpan !== 2) { + if ($long !== '') { + $newlineCount += substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1; + } + + $this->helper->addMessage( + $commentStart + $newlineCount, + Helper::SPACING_BEFORE_TAGS, + ['function'] + ); + $short = rtrim($short, $phpcsFile->eolChar . ' '); + } + } + + // Short description must be single line and end with a full stop. + $testShort = trim($short); + $lastChar = $testShort[strlen($testShort) - 1]; + if (substr_count($testShort, $phpcsFile->eolChar) !== 0) { + $this->helper->addMessage($commentStart + 1, Helper::SHORT_SINGLE_LINE, ['Function']); + } + + if (preg_match('|\p{Lu}|u', $testShort[0]) === 0) { + $this->helper->addMessage($commentStart + 1, Helper::SHORT_NOT_CAPITAL, ['Function']); + } + + if ($lastChar !== '.') { + $this->helper->addMessage($commentStart + 1, Helper::SHORT_FULL_STOP, ['Function']); + } + + // Check for unknown/deprecated tags. + // For example call: $this->processUnknownTags($commentStart, $commentEnd); + + // The last content should be a newline and the content before + // that should not be blank. If there is more blank space + // then they have additional blank lines at the end of the comment. + $words = $this->commentParser->getWords(); + $lastPos = count($words) - 1; + if (trim( + $words[$lastPos - 1] + ) !== '' || strpos( + $words[$lastPos - 1], + $this->helper->getCurrentFile()->eolChar + ) === false || trim( + $words[$lastPos - 2] + ) === '' + ) { + $this->helper->addMessage($commentEnd, Helper::SPACING_AFTER, ['function']); + } + } + + /** + * Process the see tags. + * + * @param int $commentStart The position in the stack where the comment started. + * + * @return void + */ + protected function processSees($commentStart) + { + $sees = $this->commentParser->getSees(); + if (empty($sees) === false) { + $tagOrder = $this->commentParser->getTagOrders(); + $index = array_keys($this->commentParser->getTagOrders(), 'see'); + foreach ($sees as $i => $see) { + $errorPos = $commentStart + $see->getLine(); + $since = array_keys($tagOrder, 'since'); + if (count($since) === 1 && $this->_tagIndex !== 0) { + $this->_tagIndex++; + if ($index[$i] !== $this->_tagIndex) { + $this->helper->addMessage($errorPos, Helper::SEE_ORDER); + } + } + + $content = $see->getContent(); + if (empty($content) === true) { + $this->helper->addMessage($errorPos, Helper::EMPTY_SEE, ['function']); + continue; + } + } + } + } + + /** + * Process the return comment of this function comment. + * + * @param int $commentStart The position in the stack where the comment started. + * @param int $commentEnd The position in the stack where the comment ended. + * + * @return void + */ + protected function processReturn($commentStart, $commentEnd) + { + // Skip constructor and destructor. + $className = ''; + if ($this->_classToken !== null) { + $className = $this->helper->getCurrentFile()->getDeclarationName($this->_classToken); + $className = strtolower(ltrim($className, '_')); + } + + $methodName = strtolower(ltrim($this->_methodName, '_')); + $return = $this->commentParser->getReturn(); + + if ($this->_methodName !== '__construct' && $this->_methodName !== '__destruct') { + if ($return !== null) { + $tagOrder = $this->commentParser->getTagOrders(); + $index = array_keys($tagOrder, 'return'); + $errorPos = $commentStart + $return->getLine(); + $content = trim($return->getRawContent()); + + if (count($index) > 1) { + $this->helper->addMessage($errorPos, Helper::DUPLICATE_RETURN); + return; + } + + $since = array_keys($tagOrder, 'since'); + if (count($since) === 1 && $this->_tagIndex !== 0) { + $this->_tagIndex++; + if ($index[0] !== $this->_tagIndex) { + $this->helper->addMessage($errorPos, Helper::RETURN_ORDER); + } + } + + if (empty($content) === true) { + $this->helper->addMessage($errorPos, Helper::MISSING_RETURN_TYPE); + } else { + // Strip off any comments attached to our content + $parts = explode(' ', $content); + $content = $parts[0]; + // Check return type (can be multiple, separated by '|'). + $typeNames = explode('|', $content); + $suggestedNames = []; + foreach ($typeNames as $i => $typeName) { + $suggestedName = $this->helper->suggestType($typeName); + if (in_array($suggestedName, $suggestedNames) === false) { + $suggestedNames[] = $suggestedName; + } + } + + $suggestedType = implode('|', $suggestedNames); + if ($content !== $suggestedType) { + $data = [$content]; + $this->helper->addMessage($errorPos, Helper::INVALID_RETURN, $data); + } elseif ($this->helper->isAmbiguous($typeName, $matches)) { + // Warn about ambiguous types ie array or mixed + $data = [$matches[1], '@return']; + $this->helper->addMessage($errorPos, Helper::AMBIGUOUS_TYPE, $data); + } + + $tokens = $this->helper->getCurrentFile()->getTokens(); + + // If the return type is void, make sure there is + // no return statement in the function. + if ($content === 'void') { + if (isset($tokens[$this->_functionToken]['scope_closer']) === true) { + $endToken = $tokens[$this->_functionToken]['scope_closer']; + + $tokens = $this->helper->getCurrentFile()->getTokens(); + for ($returnToken = $this->_functionToken; $returnToken < $endToken; $returnToken++) { + if ($tokens[$returnToken]['code'] === T_CLOSURE) { + $returnToken = $tokens[$returnToken]['scope_closer']; + continue; + } + + if ($tokens[$returnToken]['code'] === T_RETURN) { + break; + } + } + + if ($returnToken !== $endToken) { + // If the function is not returning anything, just + // exiting, then there is no problem. + $semicolon = $this->helper->getCurrentFile()->findNext( + T_WHITESPACE, + $returnToken + 1, + null, + true + ); + if ($tokens[$semicolon]['code'] !== T_SEMICOLON) { + $this->helper->addMessage($errorPos, Helper::INVALID_RETURN_VOID); + } + } + } + } elseif ($content !== 'mixed') { + // If return type is not void, there needs to be a + // returns statement somewhere in the function that + // returns something. + if (isset($tokens[$this->_functionToken]['scope_closer']) === true) { + $endToken = $tokens[$this->_functionToken]['scope_closer']; + $returnToken = $this->helper->getCurrentFile()->findNext( + T_RETURN, + $this->_functionToken, + $endToken + ); + if ($returnToken === false) { + $this->helper->addMessage($errorPos, Helper::INVALID_NO_RETURN); + } else { + $semicolon = $this->helper->getCurrentFile()->findNext( + T_WHITESPACE, + $returnToken + 1, + null, + true + ); + if ($tokens[$semicolon]['code'] === T_SEMICOLON) { + $this->helper->addMessage($returnToken, Helper::INVALID_RETURN_NOT_VOID); + } + } + } + } + + $spacing = substr_count($return->getWhitespaceBeforeValue(), ' '); + if ($spacing !== 1) { + $data = [$spacing]; + $this->helper->addMessage($errorPos, Helper::RETURN_INDENT, $data); + } + } + } else { + $this->helper->addMessage($commentEnd, Helper::MISSING_RETURN); + } + } elseif ($return !== null) { + // No return tag for constructor and destructor. + $errorPos = $commentStart + $return->getLine(); + $this->helper->addMessage($errorPos, Helper::RETURN_NOT_REQUIRED); + } + } + + /** + * Process any throw tags that this function comment has. + * + * @param int $commentStart The position in the stack where the comment started. + * + * @return void + */ + protected function processThrows($commentStart) + { + if (count($this->commentParser->getThrows()) === 0) { + return; + } + + $tagOrder = $this->commentParser->getTagOrders(); + $index = array_keys($this->commentParser->getTagOrders(), 'throws'); + + foreach ($this->commentParser->getThrows() as $i => $throw) { + $exception = $throw->getValue(); + $content = trim($throw->getComment()); + $errorPos = $commentStart + $throw->getLine(); + if (empty($exception) === true) { + $this->helper->addMessage($errorPos, Helper::INVALID_THROWS); + } elseif (empty($content) === true) { + $this->helper->addMessage($errorPos, Helper::EMPTY_THROWS); + } else { + // Assumes that $content is not empty. + // Starts with a capital letter and ends with a fullstop. + $firstChar = $content[0]; + if (strtoupper($firstChar) !== $firstChar) { + $this->helper->addMessage($errorPos, Helper::THROWS_NOT_CAPITAL); + } + + $lastChar = $content[strlen($content) - 1]; + if ($lastChar !== '.') { + $this->helper->addMessage($errorPos, Helper::THROWS_NO_FULL_STOP); + } + } + + $since = array_keys($tagOrder, 'since'); + if (count($since) === 1 && $this->_tagIndex !== 0) { + $this->_tagIndex++; + if ($index[$i] !== $this->_tagIndex) { + $this->helper->addMessage($errorPos, Helper::THROWS_ORDER); + } + } + } + } + + /** + * Process the function parameter comments. + * + * @param int $commentStart The position in the stack where + * the comment started. + * @param int $commentEnd The position in the stack where + * the comment ended. + * + * @return void + */ + protected function processParams($commentStart, $commentEnd) + { + $realParams = $this->helper->getCurrentFile()->getMethodParameters($this->_functionToken); + $params = $this->commentParser->getParams(); + $foundParams = []; + + if (empty($params) === false) { + $subStrCount = substr_count( + $params[count($params) - 1]->getWhitespaceAfter(), + $this->helper->getCurrentFile()->eolChar + ); + if ($subStrCount !== 2) { + $errorPos = $params[count($params) - 1]->getLine() + $commentStart; + $this->helper->addMessage($errorPos, Helper::SPACING_AFTER_PARAMS); + } + + // Parameters must appear immediately after the comment. + if ($params[0]->getOrder() !== 2) { + $errorPos = $params[0]->getLine() + $commentStart; + $this->helper->addMessage($errorPos, Helper::SPACING_BEFORE_PARAMS); + } + + $previousParam = null; + $spaceBeforeVar = 10000; + $spaceBeforeComment = 10000; + $longestType = 0; + $longestVar = 0; + + foreach ($params as $param) { + $paramComment = trim($param->getComment()); + $errorPos = $param->getLine() + $commentStart; + + // Make sure that there is only one space before the var type. + if ($param->getWhitespaceBeforeType() !== ' ') { + $this->helper->addMessage($errorPos, Helper::SPACING_BEFORE_PARAM_TYPE); + } + + $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' '); + if ($spaceCount < $spaceBeforeVar) { + $spaceBeforeVar = $spaceCount; + $longestType = $errorPos; + } + + $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' '); + + if ($spaceCount < $spaceBeforeComment && $paramComment !== '') { + $spaceBeforeComment = $spaceCount; + $longestVar = $errorPos; + } + + // Make sure they are in the correct order, and have the correct name. + $pos = $param->getPosition(); + $paramName = $param->getVarName() !== '' ? $param->getVarName() : '[ UNKNOWN ]'; + + if ($previousParam !== null) { + $previousName = $previousParam->getVarName() !== '' ? $previousParam->getVarName() : 'UNKNOWN'; + } + + // Variable must be one of the supported standard type. + $typeNames = explode('|', $param->getType()); + foreach ($typeNames as $typeName) { + $suggestedName = $this->helper->suggestType($typeName); + if ($typeName !== $suggestedName) { + $data = [$suggestedName, $typeName, $paramName, $pos]; + $this->helper->addMessage($errorPos, Helper::INCORRECT_PARAM_VAR_NAME, $data); + } elseif ($this->helper->isAmbiguous($typeName, $matches)) { + // Warn about ambiguous types ie array or mixed + $data = [$matches[1], $paramName, ' at position ' . $pos . ' is NOT recommended']; + $this->helper->addMessage($commentEnd + 2, Helper::AMBIGUOUS_TYPE, $data); + } elseif (count($typeNames) === 1) { + // Check type hint for array and custom type. + $suggestedTypeHint = ''; + if (strpos($suggestedName, 'array') !== false) { + $suggestedTypeHint = 'array'; + } elseif (strpos($suggestedName, 'callable') !== false) { + $suggestedTypeHint = 'callable'; + } elseif (in_array($typeName, $this->helper->getAllowedTypes()) === false) { + $suggestedTypeHint = $suggestedName; + } else { + $suggestedTypeHint = $this->helper->suggestType($typeName); + } + + if ($suggestedTypeHint !== '' && isset($realParams[$pos - 1]) === true) { + $typeHint = $realParams[$pos - 1]['type_hint']; + if ($typeHint === '') { + $data = [$suggestedTypeHint, $paramName, $pos]; + $this->helper->addMessage($commentEnd + 2, Helper::TYPE_HINT_MISSING, $data); + } elseif ($typeHint !== $suggestedTypeHint) { + $data = [$suggestedTypeHint, $typeHint, $paramName, $pos]; + $this->helper->addMessage($commentEnd + 2, Helper::INCORRECT_TYPE_HINT, $data); + } + } elseif ($suggestedTypeHint === '' && isset($realParams[$pos - 1]) === true) { + $typeHint = $realParams[$pos - 1]['type_hint']; + if ($typeHint !== '') { + $data = [$typeHint, $paramName, $pos]; + $this->helper->addMessage($commentEnd + 2, Helper::INVALID_TYPE_HINT, $data); + } + } + } + } + + // Make sure the names of the parameter comment matches the + // actual parameter. + if (isset($realParams[$pos - 1]) === true) { + $realName = $realParams[$pos - 1]['name']; + $foundParams[] = $realName; + + // Append ampersand to name if passing by reference. + if ($realParams[$pos - 1]['pass_by_reference'] === true) { + $realName = '&' . $realName; + } + + if ($realName !== $paramName) { + $code = Helper::PARAM_NAME_NO_MATCH; + $data = [$paramName, $realName, $pos]; + + if (strtolower($paramName) === strtolower($realName)) { + $code = Helper::PARAM_NAME_NO_CASE_MATCH; + } + + $this->helper->addMessage($errorPos, $code, $data); + } + } elseif (substr($paramName, -4) !== ',...') { + // We must have an extra parameter comment. + $this->helper->addMessage($errorPos, Helper::EXTRA_PARAM_COMMENT, [$pos]); + } + + if ($param->getVarName() === '') { + $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_NAME, [$pos]); + } + + if ($param->getType() === '') { + $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_TYPE, [$pos]); + } + + if ($paramComment === '') { + $data = [$paramName, $pos]; + $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_COMMENT, $data); + } else { + // Param comments must start with a capital letter and + // end with the full stop. + $firstChar = $paramComment[0]; + if (preg_match('|\p{Lu}|u', $firstChar) === 0) { + $this->helper->addMessage($errorPos, Helper::PARAM_COMMENT_NOT_CAPITAL); + } + $lastChar = $paramComment[strlen($paramComment) - 1]; + if ($lastChar !== '.') { + $this->helper->addMessage($errorPos, Helper::PARAM_COMMENT_FULL_STOP); + } + } + + $previousParam = $param; + } + + if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) { + $this->helper->addMessage($longestType, Helper::SPACING_AFTER_LONG_TYPE); + } + + if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) { + $this->helper->addMessage($longestVar, Helper::SPACING_AFTER_LONG_NAME); + } + } + + $realNames = []; + foreach ($realParams as $realParam) { + $realNames[] = $realParam['name']; + } + + // Report missing comments. + $diff = array_diff($realNames, $foundParams); + foreach ($diff as $neededParam) { + if (count($params) !== 0) { + $errorPos = $params[count($params) - 1]->getLine() + $commentStart; + } else { + $errorPos = $commentStart; + } + + $data = [$neededParam]; + $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_TAG, $data); + } + } +} diff --git a/dev/tests/static/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php b/dev/tests/static/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php new file mode 100644 index 000000000..e8e850706 --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php @@ -0,0 +1,28 @@ +addError('Short array syntax must be used; expected "[]" but found "array()"', $stackPtr); + } +} diff --git a/dev/tests/static/Magento/Sniffs/Files/LineLengthSniff.php b/dev/tests/static/Magento/Sniffs/Files/LineLengthSniff.php new file mode 100644 index 000000000..50020cb29 --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Files/LineLengthSniff.php @@ -0,0 +1,34 @@ +previousLineContent) !== 0; + $this->previousLineContent = $lineContent; + if (! $currentLineMatch && !$previousLineMatch) { + parent::checkLineLength($phpcsFile, $stackPtr, $lineContent); + } + } +} diff --git a/dev/tests/static/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php b/dev/tests/static/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php new file mode 100644 index 000000000..31796ec4e --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php @@ -0,0 +1,66 @@ +getTokens(); + if ($sourceFile->findPrevious(T_STRING_CONCAT, $stackPtr, $stackPtr - 3) || + $sourceFile->findNext(T_STRING_CONCAT, $stackPtr, $stackPtr + 3) + ) { + return; + } + + $content = trim($tokens[$stackPtr]['content'], "\"'"); + if (preg_match($this->literalNamespacePattern, $content) === 1 && $this->classExists($content)) { + $sourceFile->addError("Use ::class notation instead.", $stackPtr); + } + } + + /** + * @param string $className + * @return bool + */ + private function classExists($className) + { + if (!isset($this->classNames[$className])) { + $this->classNames[$className] = class_exists($className) || interface_exists($className); + } + return $this->classNames[$className]; + } +} diff --git a/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php b/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php new file mode 100644 index 000000000..095025a3d --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php @@ -0,0 +1,36 @@ +getTokens(); + if ($tokens[$stackPtr]['content'] === $this->blacklist) { + $sourceFile->addError("is_null must be avoided. Use strict comparison instead.", $stackPtr); + } + } +} diff --git a/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php b/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php new file mode 100644 index 000000000..2e3e4db2a --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php @@ -0,0 +1,42 @@ +getTokens(); + $declarationLine = $tokens[$stackPtr]['line']; + $suffixLength = strlen(self::INTERFACE_SUFFIX); + // Find first T_STRING after 'interface' keyword in the line and verify it + while ($tokens[$stackPtr]['line'] == $declarationLine) { + if ($tokens[$stackPtr]['type'] == 'T_STRING') { + if (substr($tokens[$stackPtr]['content'], 0 - $suffixLength) != self::INTERFACE_SUFFIX) { + $sourceFile->addError('Interface should have name that ends with "Interface" suffix.', $stackPtr); + } + break; + } + $stackPtr++; + } + } +} diff --git a/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php b/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php new file mode 100644 index 000000000..3d2d979c3 --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php @@ -0,0 +1,113 @@ + '7', + 'float' => '7', + 'bool' => '7', + 'string' => '7', + 'true' => '7', + 'false' => '7', + 'null' => '7', + 'void' => '7.1', + 'iterable' => '7.1', + 'resource' => '7', + 'object' => '7', + 'mixed' => '7', + 'numeric' => '7', + ]; + + /** + * {@inheritdoc} + */ + public function register() + { + return [T_CLASS, T_INTERFACE, T_TRAIT, T_NAMESPACE]; + } + + /** + * Check all namespace parts + * + * @param PHP_CodeSniffer_File $sourceFile + * @param int $stackPtr + * @return void + */ + protected function validateNamespace(PHP_CodeSniffer_File $sourceFile, $stackPtr) + { + $stackPtr += 2; + $tokens = $sourceFile->getTokens(); + while ($stackPtr < $sourceFile->numTokens && $tokens[$stackPtr]['code'] !== T_SEMICOLON) { + if ($tokens[$stackPtr]['code'] === T_WHITESPACE || $tokens[$stackPtr]['code'] === T_NS_SEPARATOR) { + $stackPtr++; //skip "namespace" and whitespace + continue; + } + $namespacePart = $tokens[$stackPtr]['content']; + if (isset($this->reservedWords[strtolower($namespacePart)])) { + $sourceFile->addError( + 'Cannot use "%s" in namespace as it is reserved since PHP %s', + $stackPtr, + 'Namespace', + [$namespacePart, $this->reservedWords[$namespacePart]] + ); + } + $stackPtr++; + } + } + + /** + * Check class name not having reserved words + * + * @param PHP_CodeSniffer_File $sourceFile + * @param int $stackPtr + * @return void + */ + protected function validateClass(PHP_CodeSniffer_File $sourceFile, $stackPtr) + { + $tokens = $sourceFile->getTokens(); + $stackPtr += 2; //skip "class" and whitespace + $className = strtolower($tokens[$stackPtr]['content']); + if (isset($this->reservedWords[$className])) { + $sourceFile->addError( + 'Cannot use "%s" as class name as it is reserved since PHP %s', + $stackPtr, + 'Class', + [$className, $this->reservedWords[$className]] + ); + } + } + + /** + * {@inheritdoc} + */ + public function process(PHP_CodeSniffer_File $sourceFile, $stackPtr) + { + $tokens = $sourceFile->getTokens(); + switch ($tokens[$stackPtr]['code']) { + case T_CLASS: + case T_INTERFACE: + case T_TRAIT: + $this->validateClass($sourceFile, $stackPtr); + break; + case T_NAMESPACE: + $this->validateNamespace($sourceFile, $stackPtr); + break; + } + } +} diff --git a/dev/tests/static/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php b/dev/tests/static/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php new file mode 100644 index 000000000..41782b1e2 --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php @@ -0,0 +1,66 @@ +getTokens(); + if ($this->doCheck($phpcsFile, $stackPtr, $tokens)) { + $previous = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); + if ($tokens[$stackPtr]['line'] - $tokens[$previous]['line'] < 2) { + $error = 'Empty line missed'; + $phpcsFile->addError($error, $stackPtr, '', null); + } + } + } + + /** + * @param PHP_CodeSniffer_File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * @return bool + */ + private function doCheck(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens) + { + $result = false; + if ($phpcsFile->hasCondition($stackPtr, T_CLASS) || $phpcsFile->hasCondition($stackPtr, T_INTERFACE)) { + $result = true; + } + + if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION)) { + $result = false; + } + $previous = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); + if ($tokens[$previous]['type'] === 'T_OPEN_CURLY_BRACKET') { + $result = false; + } + + if (strpos($tokens[$stackPtr]['content'], '/**') === false) { + $result = false; + } + + return $result; + } +} diff --git a/dev/tests/static/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php b/dev/tests/static/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php new file mode 100644 index 000000000..794f316de --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php @@ -0,0 +1,49 @@ +getTokens(); + if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION) + || $phpcsFile->hasCondition($stackPtr, T_CLASS) + || $phpcsFile->hasCondition($stackPtr, T_INTERFACE) + ) { + if ($tokens[($stackPtr - 1)]['line'] < $tokens[$stackPtr]['line'] + && $tokens[($stackPtr - 2)]['line'] === $tokens[($stackPtr - 1)]['line'] + ) { + // This is an empty line and the line before this one is not + // empty, so this could be the start of a multiple empty line block + $next = $phpcsFile->findNext(T_WHITESPACE, $stackPtr, null, true); + $lines = $tokens[$next]['line'] - $tokens[$stackPtr]['line']; + if ($lines > 1) { + $error = 'Code must not contain multiple empty lines in a row; found %s empty lines'; + $data = [$lines]; + $phpcsFile->addError($error, $stackPtr, 'MultipleEmptyLines', $data); + } + } + } + } +} diff --git a/dev/tests/static/Magento/ruleset.xml b/dev/tests/static/Magento/ruleset.xml new file mode 100644 index 000000000..794b5a8ff --- /dev/null +++ b/dev/tests/static/Magento/ruleset.xml @@ -0,0 +1,29 @@ + + + + Custom Magento2 Functional Testing Framework coding standard. + + + + + + + + + + + */_files/* + + + + + + + + + diff --git a/etc/_envs/chrome.yml b/etc/_envs/chrome.yml new file mode 100644 index 000000000..3793a2d27 --- /dev/null +++ b/etc/_envs/chrome.yml @@ -0,0 +1,9 @@ +# `chrome` environment config goes here +modules: + enabled: + - \Magento\FunctionalTestingFramework\Module\MagentoWebDriver + - \Magento\FunctionalTestingFramework\Helper\Acceptance + config: + \Magento\FunctionalTestingFramework\Module\MagentoWebDriver: + browser: chrome + window_size: maximize \ No newline at end of file diff --git a/etc/_envs/firefox.yml b/etc/_envs/firefox.yml new file mode 100644 index 000000000..e8565283a --- /dev/null +++ b/etc/_envs/firefox.yml @@ -0,0 +1,9 @@ +# `firefox` environment config goes here +modules: + enabled: + - \Magento\FunctionalTestingFramework\Module\MagentoWebDriver + - \Magento\FunctionalTestingFramework\Helper\Acceptance + config: + \Magento\FunctionalTestingFramework\Module\MagentoWebDriver: + browser: firefox + window_size: maximize \ No newline at end of file diff --git a/etc/_envs/phantomjs.yml b/etc/_envs/phantomjs.yml new file mode 100644 index 000000000..a7a65e6b5 --- /dev/null +++ b/etc/_envs/phantomjs.yml @@ -0,0 +1,8 @@ +# `phantomjs` environment config goes here +modules: + enabled: + - \Magento\FunctionalTestingFramework\Module\MagentoWebDriver + - \Magento\FunctionalTestingFramework\Helper\Acceptance + config: + \Magento\FunctionalTestingFramework\Module\MagentoWebDriver: + browser: phantomjs \ No newline at end of file diff --git a/etc/di.xml b/etc/di.xml new file mode 100644 index 000000000..081b324fc --- /dev/null +++ b/etc/di.xml @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\DataObject + arrayArgumentInterpreterProxy + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Boolean + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Number + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\StringUtils + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\NullType + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Constant + + xsi:type + + + + + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Composite + + + + + developer + + + + + converterArgumentParser + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Composite + data + + + + + + Magento\FunctionalTestingFramework\Page\Config\Data + + + + + Magento\FunctionalTestingFramework\Block\Config\Data + + + + + + Magento\FunctionalTestingFramework\Generate\GeneratePage + Magento\FunctionalTestingFramework\Generate\GenerateBlock + + + + + + + + Magento\FunctionalTestingFramework\Data\Argument\Interpreter\ArrayType + + + + + + + + Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd + + + + + Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd + + + + + Magento\FunctionalTestingFramework\Config\FileResolver\Mask + Magento\FunctionalTestingFramework\Config\Converter + Magento\FunctionalTestingFramework\Config\SchemaLocator\Page + + name + name + + #\.xml$# + Page + + + + + Magento\FunctionalTestingFramework\Config\FileResolver\Mask + Magento\FunctionalTestingFramework\Config\Converter + Magento\FunctionalTestingFramework\Config\SchemaLocator\Section + + name + name + + #\.xml$# + Section + + + + + + Magento\FunctionalTestingFramework\Page\Config\Data + + + + + + Magento\FunctionalTestingFramework\Config\Reader\Page + + + + + + Magento\FunctionalTestingFramework\Section\Config\Data + + + + + + Magento\FunctionalTestingFramework\Config\Reader\Section + + + + + + + + Magento\FunctionalTestingFramework\DataProfile\Config\Data + + + + + Magento\FunctionalTestingFramework\Config\Reader\DataProfile + + + + + Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd + + + + + Magento\FunctionalTestingFramework\Config\FileResolver\Module + Magento\FunctionalTestingFramework\Config\Converter + Magento\FunctionalTestingFramework\Config\SchemaLocator\DataProfile + + name + key + key + name + + *Data.xml + Data + + + + + + + + Magento\FunctionalTestingFramework\DataProfile\Config\Metadata + + + + + Magento\FunctionalTestingFramework\Config\Reader\Metadata + + + + + Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd + + + + + Magento\FunctionalTestingFramework\Config\FileResolver\Module + Magento\FunctionalTestingFramework\Config\Converter + Magento\FunctionalTestingFramework\Config\SchemaLocator\Metadata + + name + key + key + key + + *-meta.xml + Metadata + + + + + + + + Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd + + + + + Magento\FunctionalTestingFramework\Config\FileResolver\Module + Magento\FunctionalTestingFramework\Config\TestDataConverter + Magento\FunctionalTestingFramework\Config\SchemaLocator\TestData + + name + name + key + key + name + name + mergeKey + + *Cest.xml + Cest + + + + + + + mergeKey + mergeKey + mergeKey + name + name + key + key + name + key + key + name + key + key + name + name + + + /config/cest/annotations/env + /config/cest/annotations/features + /config/cest/annotations/stories + /config/cest/annotations/title + /config/cest/annotations/description + /config/cest/annotations/severity + /config/cest/annotations/testCaseId + /config/cest/annotations/group + /config/cest/annotations/return + /config/cest/test/annotations/features + /config/cest/test/annotations/stories + /config/cest/test/annotations/title + /config/cest/test/annotations/description + /config/cest/test/annotations/severity + /config/cest/test/annotations/testCaseId + /config/cest/test/annotations/group + /config/cest/test/annotations/env + /config/cest/test/annotations/return + + + + + + + Magento\FunctionalTestingFramework\Test\Config\Dom\ArrayNodeConfig + + + + + + Magento\FunctionalTestingFramework\Test\Config\Data + + + + + Magento\FunctionalTestingFramework\Config\Reader\TestData + + + + + + + + Magento\FunctionalTestingFramework\Config\FileResolver\Module + Magento\FunctionalTestingFramework\Config\ActionGroupDataConverter + Magento\FunctionalTestingFramework\Config\SchemaLocator\TestData + + name + name + mergeKey + + *ActionGroup.xml + ActionGroup + + + + + + + mergeKey + name + name + + + + + + + Magento\FunctionalTestingFramework\Test\Config\Dom\ActionGroupArrayNodeConfig + + + + + + Magento\FunctionalTestingFramework\Test\Config\ActionGroupData + + + + + Magento\FunctionalTestingFramework\Config\Reader\ActionGroupData + + + diff --git a/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReader.php b/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReader.php new file mode 100644 index 000000000..bc29d47fc --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReader.php @@ -0,0 +1,82 @@ +getConstructor(); + if ($constructor) { + $result = []; + /** @var $parameter \ReflectionParameter */ + foreach ($constructor->getParameters() as $parameter) { + try { + $result[] = [ + $parameter->getName(), + $parameter->getClass() !== null ? $parameter->getClass()->getName() : null, + !$parameter->isOptional(), + $parameter->isOptional() + ? ($parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null) + : null, + ]; + } catch (\ReflectionException $e) { + $message = $e->getMessage(); + throw new \ReflectionException($message, 0, $e); + } + } + } + + return $result; + } + + /** + * Retrieve parent relation information for type in a following format + * array( + * 'Parent_Class_Name', + * 'Interface_1', + * 'Interface_2', + * ... + * ) + * + * @param string $className + * @return string[] + */ + public function getParents($className) + { + $parentClass = get_parent_class($className); + if ($parentClass) { + $result = []; + $interfaces = class_implements($className); + if ($interfaces) { + $parentInterfaces = class_implements($parentClass); + if ($parentInterfaces) { + $result = array_values(array_diff($interfaces, $parentInterfaces)); + } else { + $result = array_values($interfaces); + } + } + array_unshift($result, $parentClass); + } else { + $result = array_values(class_implements($className)); + if ($result) { + array_unshift($result, null); + } else { + $result = []; + } + } + return $result; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReaderInterface.php b/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReaderInterface.php new file mode 100644 index 000000000..21472847c --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReaderInterface.php @@ -0,0 +1,34 @@ +argumentParser = $argumentParser; + $this->argumentInterpreter = $argumentInterpreter; + $this->argumentNodeName = $argumentNodeName; + $this->idAttributes = $idAttributes; + } + + /** + * Convert XML to array. + * + * @param \DOMDocument $source + * @return array + */ + public function convert($source) + { + return $this->convertXml($source->documentElement->childNodes); + } + + /** + * Convert XML node to array or string recursive. + * + * @param \DOMNodeList|array $elements + * @return array + */ + protected function convertXml($elements) + { + $result = []; + + foreach ($elements as $element) { + if ($element instanceof \DOMElement) { + if ($element->getAttribute('remove') == 'true') { + // Remove element + continue; + } + if ($element->hasAttribute('xsi:type')) { + if ($element->hasAttribute('path')) { + $elementData = $this->getAttributes($element); + $elementData['value'] = $this->argumentInterpreter->evaluate( + $this->argumentParser->parse($element) + ); + unset($elementData['xsi:type'], $elementData['item']); + } else { + $elementData = $this->argumentInterpreter->evaluate( + $this->argumentParser->parse($element) + ); + } + } else { + $elementData = array_merge( + $this->getAttributes($element), + $this->getChildNodes($element) + ); + } + $key = $this->getElementKey($element); + if ($key) { + $result[$element->nodeName][$key] = $elementData; + } elseif (!empty($elementData)) { + $result[$element->nodeName][] = $elementData; + } + } elseif ($element->nodeType == XML_TEXT_NODE && trim($element->nodeValue) != '') { + return ['value' => $element->nodeValue]; + } + } + + return $result; + } + + /** + * Get key for DOM element + * + * @param \DOMElement $element + * @return bool|string + */ + protected function getElementKey(\DOMElement $element) + { + if (isset($this->idAttributes[$element->nodeName])) { + if ($element->hasAttribute($this->idAttributes[$element->nodeName])) { + return $element->getAttribute($this->idAttributes[$element->nodeName]); + } + } + if ($element->hasAttribute(self::NAME_ATTRIBUTE)) { + return $element->getAttribute(self::NAME_ATTRIBUTE); + } + return false; + } + + /** + * Verify attribute is main key for element. + * + * @param \DOMElement $element + * @param \DOMAttr $attribute + * @return bool + */ + protected function isKeyAttribute(\DOMElement $element, \DOMAttr $attribute) + { + if (isset($this->idAttributes[$element->nodeName])) { + return $attribute->name == $this->idAttributes[$element->nodeName]; + } else { + return $attribute->name == self::NAME_ATTRIBUTE; + } + } + + /** + * Get node attributes. + * + * @param \DOMElement $element + * @return array + */ + protected function getAttributes(\DOMElement $element) + { + $attributes = []; + if ($element->hasAttributes()) { + /** @var \DomAttr $attribute */ + foreach ($element->attributes as $attribute) { + if (trim($attribute->nodeValue) != '' && !$this->isKeyAttribute($element, $attribute)) { + $attributes[$attribute->nodeName] = $this->castNumeric($attribute->nodeValue); + } + } + } + return $attributes; + } + + /** + * Get child nodes data. + * + * @param \DOMElement $element + * @return array + */ + protected function getChildNodes(\DOMElement $element) + { + $children = []; + if ($element->hasChildNodes()) { + $children = $this->convertXml($element->childNodes); + } + return $children; + } + + /** + * Cast nodeValue to int or double. + * + * @param string $nodeValue + * @return float|int + */ + protected function castNumeric($nodeValue) + { + if (is_numeric($nodeValue)) { + if (preg_match('/^\d+$/', $nodeValue)) { + $nodeValue = (int) $nodeValue; + } else { + $nodeValue = (double) $nodeValue; + } + } + + return $nodeValue; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php b/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php new file mode 100644 index 000000000..b67f543e4 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php @@ -0,0 +1,131 @@ +arrayNodeConfig = $arrayNodeConfig; + } + + /** + * Retrieve key-value pairs of node attributes + * + * @param \DOMNode $node + * @return array + */ + protected function getNodeAttributes(\DOMNode $node) + { + $result = []; + $attributes = $node->attributes ?: []; + /** @var \DOMNode $attribute */ + foreach ($attributes as $attribute) { + if ($attribute->nodeType == XML_ATTRIBUTE_NODE) { + $result[$attribute->nodeName] = $attribute->nodeValue; + } + } + return $result; + } + + /** + * Convert dom node tree to array in general case or to string in a case of a text node + * + * Example: + * + * val2 + * + * + * is converted to + * + * array( + * 'node' => array( + * 'attr' => 'wal', + * 'subnode' => 'val2' + * ) + * ) + * + * @param \DOMNode $source + * @param string $basePath + * @return string|array + * @throws \UnexpectedValueException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function convert(\DOMNode $source, $basePath = '') + { + $value = []; + /** @var \DOMNode $node */ + foreach ($source->childNodes as $node) { + if ($node->nodeType == XML_ELEMENT_NODE) { + $nodeName = $node->nodeName; + $nodePath = $basePath . '/' . $nodeName; + + $arrayKeyAttribute = $this->arrayNodeConfig->getAssocArrayKeyAttribute($nodePath); + $isNumericArrayNode = $this->arrayNodeConfig->isNumericArray($nodePath); + $isArrayNode = $isNumericArrayNode || $arrayKeyAttribute; + + if (isset($value[$nodeName]) && !$isArrayNode) { + throw new \UnexpectedValueException( + "Node path '{$nodePath}' is not unique, but it has not been marked as array." + ); + } + + $nodeData = $this->convert($node, $nodePath); + + if ($isArrayNode) { + if ($isNumericArrayNode) { + $value[$nodeName][] = $nodeData; + } elseif (isset($nodeData[$arrayKeyAttribute])) { + $arrayKeyValue = $nodeData[$arrayKeyAttribute]; + $value[$nodeName][$arrayKeyValue] = $nodeData; + } else { + throw new \UnexpectedValueException( + "Array is expected to contain value for key '{$arrayKeyAttribute}'." + ); + } + } else { + $value[$nodeName] = $nodeData; + } + } elseif ($node->nodeType == XML_CDATA_SECTION_NODE + || ($node->nodeType == XML_TEXT_NODE && trim($node->nodeValue) != '') + ) { + $value = $node->nodeValue; + break; + } + } + $result = $this->getNodeAttributes($source); + if (is_array($value)) { + $result = array_merge($result, $value); + if (!$result) { + $result = ''; + } + } else { + if ($result) { + $result['value'] = $value; + } else { + $result = $value; + } + } + return $result; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/ConverterInterface.php b/src/Magento/FunctionalTestingFramework/Config/ConverterInterface.php new file mode 100644 index 000000000..3f3c0d87a --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/ConverterInterface.php @@ -0,0 +1,20 @@ +reader = $reader; + $this->load(); + } + + /** + * Merge config data to the object + * + * @param array $config + * @return void + */ + public function merge(array $config) + { + $this->data = array_replace_recursive($this->data, $config); + } + + // @codingStandardsIgnoreStart + /** + * Get config value by key + * + * @param string $path + * + * @param null|mixed $default + * @return array|mixed|null + */ + public function get($path = null, $default = null) + { + if ($path === null) { + return $this->data; + } + $keys = explode('/', $path); + $data = $this->data; + foreach ($keys as $key) { + if (is_array($data) && array_key_exists($key, $data)) { + $data = $data[$key]; + } else { + return $default; + } + } + return $data; + } + // @codingStandardsIgnoreEnd + + /** + * Set name of the config file + * + * @param string $fileName + * @return self + */ + public function setFileName($fileName) + { + if ($fileName !== null) { + $this->reader->setFileName($fileName); + } + return $this; + } + + /** + * Load config data + * + * @param string|null $scope + * @return void + */ + public function load($scope = null) + { + $this->merge( + $this->reader->read($scope) + ); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/DataInterface.php b/src/Magento/FunctionalTestingFramework/Config/DataInterface.php new file mode 100644 index 000000000..6af873001 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/DataInterface.php @@ -0,0 +1,47 @@ + 'id_attribute_name') + * The path to ID attribute name should not include any attribute notations or modifiers -- only node names + * + * @param string $xml + * @param array $idAttributes + * @param string $typeAttributeName + * @param string $schemaFile + * @param string $errorFormat + */ + public function __construct( + $xml, + array $idAttributes = [], + $typeAttributeName = null, + $schemaFile = null, + $errorFormat = self::ERROR_FORMAT_DEFAULT + ) { + $this->schemaFile = $schemaFile; + $this->nodeMergingConfig = new Dom\NodeMergingConfig(new Dom\NodePathMatcher(), $idAttributes); + $this->typeAttributeName = $typeAttributeName; + $this->errorFormat = $errorFormat; + $this->dom = $this->initDom($xml); + $this->rootNamespace = $this->dom->lookupNamespaceUri($this->dom->namespaceURI); + } + + /** + * Merge $xml into DOM document + * + * @param string $xml + * @return void + */ + public function merge($xml) + { + $dom = $this->initDom($xml); + $this->mergeNode($dom->documentElement, ''); + } + + /** + * Recursive merging of the \DOMElement into the original document + * + * Algorithm: + * 1. Find the same node in original document + * 2. Extend and override original document node attributes and scalar value if found + * 3. Append new node if original document doesn't have the same node + * + * @param \DOMElement $node + * @param string $parentPath path to parent node + * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function mergeNode(\DOMElement $node, $parentPath) + { + $path = $this->getNodePathByParent($node, $parentPath); + + $matchedNode = $this->getMatchedNode($path); + + /* Update matched node attributes and value */ + if ($matchedNode) { + + //different node type + if ($this->typeAttributeName + && $node->hasAttribute($this->typeAttributeName) + && $matchedNode->hasAttribute($this->typeAttributeName) + && $node->getAttribute($this->typeAttributeName) + !== $matchedNode->getAttribute($this->typeAttributeName) + ) { + $this->replaceNodeValue($parentPath, $node, $matchedNode); + return; + } + + $this->mergeAttributes($matchedNode, $node); + if ($node->nodeValue === '' && $matchedNode->nodeValue !== '' && $matchedNode->childNodes->length === 1) { + $this->replaceNodeValue($parentPath, $node, $matchedNode); + } + if (!$node->hasChildNodes()) { + return; + } + /* override node value */ + if ($this->isTextNode($node)) { + /* skip the case when the matched node has children, otherwise they get overridden */ + if (!$matchedNode->hasChildNodes() || $this->isTextNode($matchedNode)) { + $matchedNode->nodeValue = $node->childNodes->item(0)->nodeValue; + } + } else { + /* recursive merge for all child nodes */ + foreach ($node->childNodes as $childNode) { + if ($childNode instanceof \DOMElement) { + $this->mergeNode($childNode, $path); + } + } + } + } else { + /* Add node as is to the document under the same parent element */ + $parentMatchedNode = $this->getMatchedNode($parentPath); + $newNode = $this->dom->importNode($node, true); + $parentMatchedNode->appendChild($newNode); + } + } + + /** + * Replace node value. + * + * @param string $parentPath + * @param \DOMElement $node + * @param \DOMElement $matchedNode + * + * @return void + */ + private function replaceNodeValue($parentPath, \DOMElement $node, \DOMElement $matchedNode) + { + $parentMatchedNode = $this->getMatchedNode($parentPath); + $newNode = $this->dom->importNode($node, true); + $parentMatchedNode->replaceChild($newNode, $matchedNode); + } + + /** + * Check if the node content is text + * + * @param \DOMElement $node + * @return bool + */ + protected function isTextNode($node) + { + return $node->childNodes->length == 1 && $node->childNodes->item(0) instanceof \DOMText; + } + + /** + * Merges attributes of the merge node to the base node + * + * @param \DOMElement $baseNode + * @param \DOMNode $mergeNode + * @return void + */ + protected function mergeAttributes($baseNode, $mergeNode) + { + foreach ($mergeNode->attributes as $attribute) { + $baseNode->setAttribute($this->getAttributeName($attribute), $attribute->value); + } + } + + /** + * Identify node path based on parent path and node attributes + * + * @param \DOMElement $node + * @param string $parentPath + * @return string + */ + protected function getNodePathByParent(\DOMElement $node, $parentPath) + { + $prefix = $this->rootNamespace === null ? '' : self::ROOT_NAMESPACE_PREFIX . ':'; + $path = $parentPath . '/' . $prefix . $node->tagName; + $idAttribute = $this->nodeMergingConfig->getIdAttribute($path); + if ($idAttribute) { + foreach (explode('|', $idAttribute) as $idAttributeValue) { + if ($value = $node->getAttribute($idAttributeValue)) { + $path .= "[@{$idAttributeValue}='{$value}']"; + break; + } + } + } + return $path; + } + + /** + * Getter for node by path + * An exception is possible if original document contains multiple nodes for identifier + * + * @param string $nodePath + * @throws \Exception + * @return \DOMElement|null + */ + protected function getMatchedNode($nodePath) + { + $xPath = new \DOMXPath($this->dom); + if ($this->rootNamespace) { + $xPath->registerNamespace(self::ROOT_NAMESPACE_PREFIX, $this->rootNamespace); + } + $matchedNodes = $xPath->query($nodePath); + $node = null; + if ($matchedNodes->length > 1) { + throw new \Exception("More than one node matching the query: {$nodePath}"); + } elseif ($matchedNodes->length == 1) { + $node = $matchedNodes->item(0); + } + return $node; + } + + /** + * Validate dom document + * + * @param \DOMDocument $dom + * @param string $schemaFileName + * @param string $errorFormat + * @return array of errors + * @throws \Exception + */ + public static function validateDomDocument( + \DOMDocument $dom, + $schemaFileName, + $errorFormat = self::ERROR_FORMAT_DEFAULT + ) { + libxml_use_internal_errors(true); + try { + $result = $dom->schemaValidate($schemaFileName); + $errors = []; + if (!$result) { + $validationErrors = libxml_get_errors(); + if (count($validationErrors)) { + foreach ($validationErrors as $error) { + $errors[] = self::renderErrorMessage($error, $errorFormat); + } + } else { + $errors[] = 'Unknown validation error'; + } + } + } catch (\Exception $exception) { + libxml_use_internal_errors(false); + throw new \Exception( + sprintf( + 'Failed to validate xml using schema: %s. Exception: %s', + $schemaFileName, + $exception->getMessage() + ) + ); + } + libxml_use_internal_errors(false); + return $errors; + } + + /** + * Render error message string by replacing placeholders '%field%' with properties of \LibXMLError + * + * @param \LibXMLError $errorInfo + * @param string $format + * @return string + * @throws \InvalidArgumentException + */ + private static function renderErrorMessage(\LibXMLError $errorInfo, $format) + { + $result = $format; + foreach ($errorInfo as $field => $value) { + $placeholder = '%' . $field . '%'; + $value = trim((string)$value); + $result = str_replace($placeholder, $value, $result); + } + if (strpos($result, '%') !== false) { + throw new \InvalidArgumentException("Error format '{$format}' contains unsupported placeholders."); + } + return $result; + } + + /** + * DOM document getter + * + * @return \DOMDocument + */ + public function getDom() + { + return $this->dom; + } + + /** + * Create DOM document based on $xml parameter + * + * @param string $xml + * @return \DOMDocument + * @throws \Magento\FunctionalTestingFramework\Config\Dom\ValidationException + */ + protected function initDom($xml) + { + $dom = new \DOMDocument(); + $dom->loadXML($xml); + if ($this->schemaFile) { + $errors = self::validateDomDocument($dom, $this->schemaFile, $this->errorFormat); + if (count($errors)) { + throw new \Magento\FunctionalTestingFramework\Config\Dom\ValidationException(implode("\n", $errors)); + } + } + return $dom; + } + + /** + * Validate self contents towards to specified schema + * + * @param string $schemaFileName absolute path to schema file + * @param array &$errors + * @return bool + */ + public function validate($schemaFileName, &$errors = []) + { + $errors = self::validateDomDocument($this->dom, $schemaFileName, $this->errorFormat); + return !count($errors); + } + + /** + * Set schema file + * + * @param string $schemaFile + * @return $this + */ + public function setSchemaFile($schemaFile) + { + $this->schemaFile = $schemaFile; + return $this; + } + + /** + * Returns the attribute name with prefix, if there is one + * + * @param \DOMAttr $attribute + * @return string + */ + private function getAttributeName($attribute) + { + if ($attribute->prefix !== null && !empty($attribute->prefix)) { + $attributeName = $attribute->prefix . ':' . $attribute->name; + } else { + $attributeName = $attribute->name; + } + return $attributeName; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php b/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php new file mode 100644 index 000000000..eaa216649 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php @@ -0,0 +1,81 @@ + '', ...) + * + * @var array + */ + private $assocArrays = []; + + /** + * Format: array('/numeric/array/path', ...) + * + * @var array + */ + private $numericArrays = []; + + /** + * ArrayNodeConfig constructor. + * @param NodePathMatcher $nodePathMatcher + * @param array $assocArrayAttributes + * @param array $numericArrays + */ + public function __construct( + NodePathMatcher $nodePathMatcher, + array $assocArrayAttributes, + array $numericArrays = [] + ) { + $this->nodePathMatcher = $nodePathMatcher; + $this->assocArrays = $assocArrayAttributes; + $this->numericArrays = $numericArrays; + } + + /** + * Whether a node is a numeric array or not + * + * @param string $nodeXpath + * @return bool + */ + public function isNumericArray($nodeXpath) + { + foreach ($this->numericArrays as $pathPattern) { + if ($this->nodePathMatcher->match($pathPattern, $nodeXpath)) { + return true; + } + } + return false; + } + + /** + * Retrieve name of array key attribute, if a node is an associative array + * + * @param string $nodeXpath + * @return string|null + */ + public function getAssocArrayKeyAttribute($nodeXpath) + { + foreach ($this->assocArrays as $pathPattern => $keyAttribute) { + if ($this->nodePathMatcher->match($pathPattern, $nodeXpath)) { + return $keyAttribute; + } + } + return null; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php b/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php new file mode 100644 index 000000000..f880680fb --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php @@ -0,0 +1,53 @@ + '', ...) + * + * @var array + */ + private $idAttributes = []; + + /** + * NodeMergingConfig constructor. + * @param NodePathMatcher $nodePathMatcher + * @param array $idAttributes + */ + public function __construct(NodePathMatcher $nodePathMatcher, array $idAttributes) + { + $this->nodePathMatcher = $nodePathMatcher; + $this->idAttributes = $idAttributes; + } + + /** + * Retrieve name of an identifier attribute for a node + * + * @param string $nodeXpath + * @return string|null + */ + public function getIdAttribute($nodeXpath) + { + foreach ($this->idAttributes as $pathPattern => $idAttribute) { + if ($this->nodePathMatcher->match($pathPattern, $nodeXpath)) { + return $idAttribute; + } + } + return null; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php b/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php new file mode 100644 index 000000000..bf70de73e --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php @@ -0,0 +1,40 @@ +simplifyXpath($xpathSubject); + $pathPattern = '#^' . $pathPattern . '$#'; + return (bool)preg_match($pathPattern, $pathSubject); + } + + /** + * Strip off predicates and namespaces from the XPath + * + * @param string $xpath + * @return string + */ + protected function simplifyXpath($xpath) + { + $result = $xpath; + $result = preg_replace('/\[@[^\]]+?\]/', '', $result); + $result = preg_replace('/\/[^:]+?\:/', '/', $result); + return $result; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/ValidationException.php b/src/Magento/FunctionalTestingFramework/Config/Dom/ValidationException.php new file mode 100644 index 000000000..18498c8b7 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/ValidationException.php @@ -0,0 +1,14 @@ +moduleResolver = $moduleResolver; + } else { + $this->moduleResolver = ModuleResolver::getInstance(); + } + } + + /** + * Retrieve the list of configuration files with given name that relate to specified scope + * + * @param string $filename + * @param string $scope + * @return array|\Iterator,\Countable + */ + public function get($filename, $scope) + { + $paths = $this->getFileCollection($filename, $scope); + + return new File($paths); + } + + /** + * Get scope of paths. + * + * @param string $filename + * @param string $scope + * @return array + */ + protected function getFileCollection($filename, $scope) + { + $paths = []; + $modulesPath = $this->moduleResolver->getModulesPath(); + + foreach ($modulesPath as $modulePath) { + $path = $modulePath . '/' . $scope . '/'; + if (is_readable($path)) { + $directoryIterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $path, + \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS + ) + ); + $regexpIterator = new \RegexIterator($directoryIterator, $filename); + /** @var \SplFileInfo $file */ + foreach ($regexpIterator as $file) { + if ($file->isFile() && $file->isReadable()) { + $paths[] = $file->getRealPath(); + } + } + } + } + + return $this->moduleResolver->sortFilesByModuleSequence($paths); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php new file mode 100644 index 000000000..9921fd42c --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php @@ -0,0 +1,53 @@ +moduleResolver = ModuleResolver::getInstance(); + } + + /** + * Retrieve the list of configuration files with given name that relate to specified scope. + * + * @param string $filename + * @param string $scope + * @return array|\Iterator,\Countable + */ + public function get($filename, $scope) + { + $modulesPath = $this->moduleResolver->getModulesPath(); + $paths = []; + foreach ($modulesPath as $modulePath) { + $path = $modulePath . DIRECTORY_SEPARATOR . $scope . DIRECTORY_SEPARATOR . $filename; + $paths = array_merge($paths, glob($path)); + } + + $iterator = new File($paths); + return $iterator; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Primary.php b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Primary.php new file mode 100644 index 000000000..14643b32e --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Primary.php @@ -0,0 +1,77 @@ +getFilePaths($filename, $scope)); + } + + /** + * Get list of configuration files + * + * @param string $filename + * @param string $scope + * @return array + */ + private function getFilePaths($filename, $scope) + { + $paths = []; + foreach ($this->getPathPatterns($filename, $scope) as $pattern) { + $paths = array_merge($paths, glob($pattern)); + } + return array_combine($paths, $paths); + } + + /** + * Retrieve patterns for glob function + * + * @param string $filename + * @param string $scope + * @return array + */ + private function getPathPatterns($filename, $scope) + { + if (substr($scope, 0, strlen(FW_BP)) === FW_BP) { + $patterns = [ + $scope . '/' . $filename, + $scope . '/*/' . $filename + ]; + } else { + $defaultPath = dirname(dirname(dirname(dirname(__DIR__)))); + $defaultPath = str_replace('\\', '/', $defaultPath); + $patterns = [ + $defaultPath . '/' . $scope . '/' . $filename, + $defaultPath . '/' . $scope . '/*/' . $filename, + FW_BP . '/' . $scope . '/' . $filename, + FW_BP . '/' . $scope . '/*/' . $filename + ]; + } + return str_replace('/', DIRECTORY_SEPARATOR, $patterns); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolverInterface.php b/src/Magento/FunctionalTestingFramework/Config/FileResolverInterface.php new file mode 100644 index 000000000..31fd9b94c --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolverInterface.php @@ -0,0 +1,21 @@ + 'name', + '/scenarios/scenario/methods/method' => 'name', + '/scenarios/scenario/methods/method/steps/step' => 'name', + ]; + + /** + * Reader constructor. + * @param FileResolverInterface $fileResolver + * @param ConverterInterface $converter + * @param SchemaLocatorInterface $schemaLocator + * @param ValidationStateInterface $validationState + * @param string $fileName + * @param array $idAttributes + * @param string $domDocumentClass + * @param string $defaultScope + */ + public function __construct( + FileResolverInterface $fileResolver, + ConverterInterface $converter, + SchemaLocatorInterface $schemaLocator, + ValidationStateInterface $validationState, + $fileName = 'scenario.xml', + $idAttributes = [], + $domDocumentClass = 'Magento\FunctionalTestingFramework\Config\Dom', + $defaultScope = 'etc' + ) { + $this->fileResolver = $fileResolver; + $this->converter = $converter; + $this->fileName = $fileName; + $this->idAttributes = array_replace($this->idAttributes, $idAttributes); + $this->schemaFile = $schemaLocator->getSchema(); + $this->isValidated = $validationState->isValidated(); + $this->perFileSchema = $schemaLocator->getPerFileSchema() && + $this->isValidated ? $schemaLocator->getPerFileSchema() : null; + $this->domDocumentClass = $domDocumentClass; + $this->defaultScope = $defaultScope; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php new file mode 100644 index 000000000..9b86c6de1 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php @@ -0,0 +1,195 @@ +fileResolver = $fileResolver; + $this->converter = $converter; + $this->fileName = $fileName; + $this->idAttributes = array_replace($this->idAttributes, $idAttributes); + $this->validationState = $validationState; + $this->schemaFile = $schemaLocator->getSchema(); + $this->perFileSchema = $schemaLocator->getPerFileSchema() && $validationState->isValidationRequired() + ? $schemaLocator->getPerFileSchema() : null; + $this->domDocumentClass = $domDocumentClass; + $this->defaultScope = $defaultScope; + } + + /** + * Load configuration scope + * + * @param string|null $scope + * @return array + */ + public function read($scope = null) + { + $scope = $scope ?: $this->defaultScope; + $fileList = $this->fileResolver->get($this->fileName, $scope); + if (!count($fileList)) { + return []; + } + $output = $this->readFiles($fileList); + + return $output; + } + + /** + * Read configuration files + * + * @param array $fileList + * @return array + * @throws \Exception + */ + protected function readFiles($fileList) + { + /** @var \Magento\FunctionalTestingFramework\Config\Dom $configMerger */ + $configMerger = null; + foreach ($fileList as $key => $content) { + try { + if (!$configMerger) { + $configMerger = $this->createConfigMerger($this->domDocumentClass, $content); + } else { + $configMerger->merge($content); + } + } catch (\Magento\FunctionalTestingFramework\Config\Dom\ValidationException $e) { + throw new \Exception("Invalid XML in file " . $key . ":\n" . $e->getMessage()); + } + } + if ($this->validationState->isValidationRequired()) { + $errors = []; + if ($configMerger && !$configMerger->validate($this->schemaFile, $errors)) { + $message = "Invalid Document \n"; + throw new \Exception($message . implode("\n", $errors)); + } + } + + $output = []; + if ($configMerger) { + $output = $this->converter->convert($configMerger->getDom()); + } + return $output; + } + + /** + * Return newly created instance of a config merger + * + * @param string $mergerClass + * @param string $initialContents + * @return \Magento\FunctionalTestingFramework\Config\Dom + * @throws \UnexpectedValueException + */ + protected function createConfigMerger($mergerClass, $initialContents) + { + $result = new $mergerClass( + $initialContents, + $this->idAttributes, + null, + $this->perFileSchema + ); + if (!$result instanceof \Magento\FunctionalTestingFramework\Config\Dom) { + throw new \UnexpectedValueException( + "Instance of the DOM config merger is expected, got {$mergerClass} instead." + ); + } + return $result; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/ReaderInterface.php b/src/Magento/FunctionalTestingFramework/Config/ReaderInterface.php new file mode 100644 index 000000000..9e64a6715 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/ReaderInterface.php @@ -0,0 +1,23 @@ +schemaPath = FW_BP . '/' . $schemaPath; + } else { + $path = dirname(dirname(dirname(__DIR__))); + $path = str_replace('\\', '/', $path); + $this->schemaPath = $path . '/' . $schemaPath; + } + } + + /** + * Get path to merged config schema + * + * @return string + */ + public function getSchema() + { + return $this->schemaPath; + } + + /** + * Get path to pre file validation schema + * + * @return null + */ + public function getPerFileSchema() + { + return null; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/SchemaLocatorInterface.php b/src/Magento/FunctionalTestingFramework/Config/SchemaLocatorInterface.php new file mode 100644 index 000000000..98ef8e73e --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/SchemaLocatorInterface.php @@ -0,0 +1,27 @@ +appMode = $appMode; + } + + /** + * Retrieve current validation state + * + * @return boolean + */ + public function isValidationRequired() + { + return $this->appMode == 'developer'; // @todo + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/ValidationStateInterface.php b/src/Magento/FunctionalTestingFramework/Config/ValidationStateInterface.php new file mode 100644 index 000000000..2bcb2adf0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/ValidationStateInterface.php @@ -0,0 +1,19 @@ +constInterpreter = $constInterpreter; + } + + /** + * Compute and return effective value of an argument. + * + * @param array $data + * @return array + */ + public function evaluate(array $data) + { + return ['argument' => $this->constInterpreter->evaluate($data)]; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php new file mode 100644 index 000000000..98fcf05e2 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php @@ -0,0 +1,124 @@ +itemInterpreter = $itemInterpreter; + } + + /** + * {@inheritdoc} + * @return array + * @throws \InvalidArgumentException + */ + public function evaluate(array $data) + { + $items = isset($data['item']) ? $data['item'] : []; + if (!is_array($items)) { + throw new \InvalidArgumentException('Array items are expected.'); + } + $result = []; + $items = $this->sortItems($items); + foreach ($items as $itemKey => $itemData) { + $result[$itemKey] = $this->itemInterpreter->evaluate($itemData); + } + return $result; + } + + /** + * Sort items by sort order attribute. + * + * @param array $items + * @return array + */ + private function sortItems($items) + { + $sortOrderDefined = $this->isSortOrderDefined($items); + if ($sortOrderDefined) { + $indexedItems = []; + foreach ($items as $key => $item) { + $indexedItems[] = ['key' => $key, 'item' => $item]; + } + uksort( + $indexedItems, + function ($firstItemKey, $secondItemKey) use ($indexedItems) { + return $this->compareItems($firstItemKey, $secondItemKey, $indexedItems); + } + ); + // Convert array of sorted items back to initial format + $items = []; + foreach ($indexedItems as $indexedItem) { + $items[$indexedItem['key']] = $indexedItem['item']; + } + } + return $items; + } + + /** + * Compare sortOrder of item + * + * @param string|int $firstItemKey + * @param string|int $secondItemKey + * @param array $indexedItems + * @return int + */ + private function compareItems($firstItemKey, $secondItemKey, $indexedItems) + { + $firstItem = $indexedItems[$firstItemKey]['item']; + $secondItem = $indexedItems[$secondItemKey]['item']; + $firstValue = 0; + $secondValue = 0; + if (isset($firstItem['sortOrder'])) { + $firstValue = intval($firstItem['sortOrder']); + } + + if (isset($secondItem['sortOrder'])) { + $secondValue = intval($secondItem['sortOrder']); + } + + if ($firstValue == $secondValue) { + // These keys reflect initial relative position of items. + // Allows stable sort for items with equal 'sortOrder' + return $firstItemKey < $secondItemKey ? -1 : 1; + } + return $firstValue < $secondValue ? -1 : 1; + } + + /** + * Determine if a sort order exists for any of the items. + * + * @param array $items + * @return bool + */ + private function isSortOrderDefined($items) + { + foreach ($items as $itemData) { + if (isset($itemData['sortOrder'])) { + return true; + } + } + return false; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Boolean.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Boolean.php new file mode 100644 index 000000000..f545c2aa4 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Boolean.php @@ -0,0 +1,45 @@ +booleanUtils = $booleanUtils; + } + + /** + * {@inheritdoc} + * @return bool + * @throws \InvalidArgumentException + */ + public function evaluate(array $data) + { + if (!isset($data['value'])) { + throw new \InvalidArgumentException('Boolean value is missing.'); + } + $value = $data['value']; + return $this->booleanUtils->toBoolean($value); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Composite.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Composite.php new file mode 100644 index 000000000..2d3ea0e0e --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Composite.php @@ -0,0 +1,95 @@ +' => , ...) + * + * @var InterpreterInterface[] + */ + private $interpreters; + + /** + * Data key that holds name of an interpreter to be used for that data + * + * @var string + */ + private $discriminator; + + /** + * Composite constructor. + * @param array $interpreters + * @param string $discriminator + * @throws \InvalidArgumentException + */ + public function __construct(array $interpreters, $discriminator) + { + foreach ($interpreters as $interpreterName => $interpreterInstance) { + if (!$interpreterInstance instanceof InterpreterInterface) { + throw new \InvalidArgumentException( + "Interpreter named '{$interpreterName}' is expected to be an argument interpreter instance." + ); + } + } + $this->interpreters = $interpreters; + $this->discriminator = $discriminator; + } + + /** + * {@inheritdoc} + * @throws \InvalidArgumentException + */ + public function evaluate(array $data) + { + if (!isset($data[$this->discriminator])) { + throw new \InvalidArgumentException( + sprintf('Value for key "%s" is missing in the argument data.', $this->discriminator) + ); + } + $interpreterName = $data[$this->discriminator]; + unset($data[$this->discriminator]); + $interpreter = $this->getInterpreter($interpreterName); + return $interpreter->evaluate($data); + } + + /** + * Register interpreter instance under a given unique name + * + * @param string $name + * @param InterpreterInterface $instance + * @return void + * @throws \InvalidArgumentException + */ + public function addInterpreter($name, InterpreterInterface $instance) + { + if (isset($this->interpreters[$name])) { + throw new \InvalidArgumentException("Argument interpreter named '{$name}' has already been defined."); + } + $this->interpreters[$name] = $instance; + } + + /** + * Retrieve interpreter instance by its unique name + * + * @param string $name + * @return InterpreterInterface + * @throws \InvalidArgumentException + */ + protected function getInterpreter($name) + { + if (!isset($this->interpreters[$name])) { + throw new \InvalidArgumentException("Argument interpreter named '{$name}' has not been defined."); + } + return $this->interpreters[$name]; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Constant.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Constant.php new file mode 100644 index 000000000..05734375b --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Constant.php @@ -0,0 +1,27 @@ +booleanUtils = $booleanUtils; + } + + /** + * Compute and return effective value of an argument + * + * @param array $data + * @return array + * @throws \InvalidArgumentException + * @throws \UnexpectedValueException + */ + public function evaluate(array $data) + { + $result = ['instance' => $data['value']]; + if (isset($data['shared'])) { + $result['shared'] = $this->booleanUtils->toBoolean($data['shared']); + } + return $result; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/NullType.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/NullType.php new file mode 100644 index 000000000..f1985cf3f --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/NullType.php @@ -0,0 +1,24 @@ +objectManager = $objectManager; + $this->instanceName = $instanceName; + $this->isShared = $shared; + } + + /** + * Definition of field which should be serialized. + * + * @return array + */ + public function __sleep() + { + return ['subject', 'isShared']; + } + + /** + * Retrieve ObjectManager from global scope + * @return void + */ + public function __wakeup() + { + $this->objectManager = \Magento\FunctionalTestingFramework\ObjectManager::getInstance(); + } + + /** + * Clone proxied instance + * @return void + */ + public function __clone() + { + $this->subject = clone $this->getSubject(); + } + + /** + * Get proxied instance + * + * @return \Magento\FunctionalTestingFramework\Data\Argument\InterpreterInterface + */ + protected function getSubject() + { + if (!$this->subject) { + $this->subject = true === $this->isShared + ? $this->objectManager->get($this->instanceName) + : $this->objectManager->create($this->instanceName); + } + return $this->subject; + } + + /** + * {@inheritdoc} + */ + public function evaluate(array $data) + { + return $this->getSubject()->evaluate($data); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Data/etc/types.xsd b/src/Magento/FunctionalTestingFramework/Data/etc/types.xsd new file mode 100644 index 000000000..b7fbc31ee --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Data/etc/types.xsd @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Api/ApiExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Api/ApiExecutor.php new file mode 100644 index 000000000..ee0f6eb81 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Api/ApiExecutor.php @@ -0,0 +1,317 @@ +operation = $operation; + $this->entityObject = $entityObject; + if ($dependentEntities != null) { + foreach ($dependentEntities as $entity) { + $this->dependentEntities[$entity->getName()] = $entity; + } + } + + $this->jsonDefinition = JsonDefinitionObjectHandler::getInstance()->getJsonDefinition( + $this->operation, + $this->entityObject->getType() + ); + } + + /** + * Executes an api request based on parameters given by constructor. + * + * @return string | null + */ + public function executeRequest() + { + $apiClientUrl = $this->jsonDefinition->getApiUrl(); + + $matchedParams = []; + preg_match_all("/[{](.+?)[}]/", $apiClientUrl, $matchedParams); + + if (!empty($matchedParams)) { + foreach ($matchedParams[0] as $paramKey => $paramValue) { + $param = $this->entityObject->getDataByName( + $matchedParams[1][$paramKey], + EntityDataObject::CEST_UNIQUE_VALUE + ); + $apiClientUrl = str_replace($paramValue, $param, $apiClientUrl); + } + } + + $authorization = $this->jsonDefinition->getAuth(); + $headers = $this->jsonDefinition->getHeaders(); + + if ($authorization) { + $headers[] = $this->getAuthorizationHeader($authorization); + } + + $jsonBody = $this->getEncodedJsonString(); + + $apiClientUtil = new ApiClientUtil( + $apiClientUrl, + $headers, + $this->jsonDefinition->getApiMethod(), + empty($jsonBody) ? null : $jsonBody + ); + + return $apiClientUtil->submit(); + } + + /** + * Returns the authorization token needed for some requests via REST call. + * + * @param string $authUrl + * @return string + */ + private function getAuthorizationHeader($authUrl) + { + $headers = ['Content-Type: application/json']; + $authCreds = [ + 'username' => getenv('MAGENTO_ADMIN_USERNAME'), + 'password' => getenv('MAGENTO_ADMIN_PASSWORD') + ]; + + $apiClientUtil = new ApiClientUtil($authUrl, $headers, 'POST', json_encode($authCreds)); + $token = $apiClientUtil->submit(); + $authHeader = 'Authorization: Bearer ' . str_replace('"', "", $token); + + return $authHeader; + } + + /** + * This function returns an array which is structurally equal to the json which is needed by the web api for + * entity creation. The function retrieves an array describing the json metadata and traverses any dependencies + * recursively forming an array which represents the json structure for the api of the desired type. + * + * @param EntityDataObject $entityObject + * @param array $jsonArrayMetadata + * @return array + */ + private function convertJsonArray($entityObject, $jsonArrayMetadata) + { + $jsonArray = []; + self::incrementSequence($entityObject->getName()); + + foreach ($jsonArrayMetadata as $jsonElement) { + if ($jsonElement->getType() == JsonObjectExtractor::JSON_OBJECT_OBJ_NAME) { + $jsonArray[$jsonElement->getValue()] = + $this->convertJsonArray($entityObject, $jsonElement->getNestedMetadata()); + } + + $jsonElementType = $jsonElement->getValue(); + + if (in_array($jsonElementType, ApiExecutor::PRIMITIVE_TYPES)) { + $elementData = $entityObject->getDataByName( + $jsonElement->getKey(), + EntityDataObject::CEST_UNIQUE_VALUE + ); + + if (array_key_exists($jsonElement->getKey(), $entityObject->getUniquenessData())) { + $uniqueData = $entityObject->getUniquenessDataByName($jsonElement->getKey()); + if ($uniqueData === 'suffix') { + $elementData .= (string)self::getSequence($entityObject->getName()); + } else { + $elementData = (string)self::getSequence($entityObject->getName()) + . $elementData; + } + } + + $jsonArray[$jsonElement->getKey()] = $this->castValue($jsonElementType, $elementData); + } else { + $entityNamesOfType = $entityObject->getLinkedEntitiesOfType($jsonElementType); + + foreach ($entityNamesOfType as $entityName) { + $jsonDataSubArray = $this->resolveNonPrimitiveElement($entityName, $jsonElement); + + if ($jsonElement->getType() == 'array') { + $jsonArray[$jsonElement->getKey()][] = $jsonDataSubArray; + } else { + $jsonArray[$jsonElement->getKey()] = $jsonDataSubArray; + } + } + } + } + + return $jsonArray; + } + + /** + * Resolves JsonObjects and pre-defined metadata (in other operation.xml file) referenced by the json metadata + * + * @param string $entityName + * @param JsonElement $jsonElement + * @return array + */ + private function resolveNonPrimitiveElement($entityName, $jsonElement) + { + $linkedEntityObj = $this->resolveLinkedEntityObject($entityName); + + if (!empty($jsonElement->getNestedJsonElement($jsonElement->getValue())) + && $jsonElement->getType() == 'array') { + $jsonSubArray = $this->convertJsonArray( + $linkedEntityObj, + [$jsonElement->getNestedJsonElement($jsonElement->getValue())] + ); + + return $jsonSubArray[$jsonElement->getValue()]; + } + + $jsonMetadata = JsonDefinitionObjectHandler::getInstance()->getJsonDefinition( + $this->operation, + $linkedEntityObj->getType() + )->getJsonMetadata(); + + return $this->convertJsonArray($linkedEntityObj, $jsonMetadata); + } + + /** + * Method to wrap entity resolution, checks locally defined dependent entities first + * + * @param string $entityName + * @return EntityDataObject + */ + private function resolveLinkedEntityObject($entityName) + { + // check our dependent entity list to see if we have this defined + if (array_key_exists($entityName, $this->dependentEntities)) { + return $this->dependentEntities[$entityName]; + } + + return DataObjectHandler::getInstance()->getObject($entityName); + } + + /** + * This function retrieves an array representative of json body for a request and returns it encoded as a string. + * + * @return string + */ + public function getEncodedJsonString() + { + $jsonMetadataArray = $this->convertJsonArray($this->entityObject, $this->jsonDefinition->getJsonMetadata()); + + return json_encode($jsonMetadataArray, JSON_PRETTY_PRINT); + } + + /** + * Increment an entity's sequence number by 1. + * + * @param string $entityName + * @return void + */ + private static function incrementSequence($entityName) + { + if (array_key_exists($entityName, self::$entitySequences)) { + self::$entitySequences[$entityName]++; + } else { + self::$entitySequences[$entityName] = 1; + } + } + + /** + * Get the current sequence number for an entity. + * + * @param string $entityName + * @return int + */ + private static function getSequence($entityName) + { + if (array_key_exists($entityName, self::$entitySequences)) { + return self::$entitySequences[$entityName]; + } + return 0; + } + + // @codingStandardsIgnoreStart + /** + * This function takes a string value and its corresponding type and returns the string cast + * into its the type passed. + * + * @param string $type + * @param string $value + * @return mixed + */ + private function castValue($type, $value) + { + $newVal = $value; + + switch ($type) { + case 'string': + break; + case 'integer': + $newVal = (integer)$value; + break; + case 'boolean': + $newVal = (boolean)$value; + break; + case 'double': + $newVal = (double)$value; + break; + } + + return $newVal; + } + // @codingStandardsIgnoreEnd +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Api/EntityApiHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Api/EntityApiHandler.php new file mode 100644 index 000000000..8116dbd87 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Api/EntityApiHandler.php @@ -0,0 +1,107 @@ +entityObject = clone $entityObject; + $this->dependentObjects = $dependentObjects; + } + + /** + * Function which executes a create request based on specific operation metadata + * @return void + */ + public function createEntity() + { + $apiExecutor = new ApiExecutor('create', $this->entityObject, $this->dependentObjects); + $result = $apiExecutor->executeRequest(); + + $this->createdObject = new EntityDataObject( + $this->entityObject->getName(), + $this->entityObject->getType(), + json_decode($result, true), + null, + null // No uniqueness data is needed to be further processed. + ); + } + + /** + * Function which executes a delete request based on specific operation metadata + * + * @return string | false + */ + public function deleteEntity() + { + $apiExecutor = new ApiExecutor('delete', $this->createdObject); + $result = $apiExecutor->executeRequest(); + + return $result; + } + + /** + * Returns the createdDataObject, instantiated when the entity is created via API. + * @return EntityDataObject + */ + public function getCreatedObject() + { + return $this->createdObject; + } + + /** + * Returns a specific data value based on the CreatedObject's definition. + * @param string $dataName + * @return string + */ + public function getCreatedDataByName($dataName) + { + $data = $this->createdObject->getDataByName($dataName, EntityDataObject::NO_UNIQUE_PROCESS); + if (empty($data)) { + $data = $this->entityObject->getDataByName($dataName, EntityDataObject::CEST_UNIQUE_VALUE); + } + return $data; + } + + // TODO add update function + /* public function updateEntity() + { + + }*/ +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php new file mode 100644 index 000000000..633028538 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php @@ -0,0 +1,203 @@ +create(DataProfileSchemaParser::class); + $entityParsedData = $entityParser->readDataProfiles(); + + if (!$entityParsedData) { + throw new \Exception("No entities could be parsed from xml definitions"); + } + + self::$DATA_OBJECT_HANDLER = new DataObjectHandler($entityParsedData); + } + + return self::$DATA_OBJECT_HANDLER; + } + + /** + * DataArrayProcessor constructor. + * @constructor + * @param array $arrayData + */ + private function __construct($arrayData) + { + $this->arrayData = $arrayData; + } + + /** + * Retrieves the object representation of data represented in data.xml + * @param string $entityName + * @return EntityDataObject | null + */ + public function getObject($entityName) + { + if (array_key_exists($entityName, $this->getAllObjects())) { + return $this->getAllObjects()[$entityName]; + } + + return null; + } + + /** + * Retrieves all object representations of all data represented in data.xml + * @return array + */ + public function getAllObjects() + { + if (!$this->data) { + $this->parseEnvVariables(); + $this->parseDataEntities(); + } + + return $this->data; + } + + /** + * Adds all .env variables defined in the PROJECT_ROOT as EntityDataObjects. This is to allow resolution + * of these variables when referenced in a cest. + * @return void + */ + private function parseEnvVariables() + { + $envFilename = PROJECT_ROOT . '/.env'; + if (file_exists($envFilename)) { + $envData = []; + $envFile = file($envFilename); + foreach ($envFile as $entry) { + $params = explode("=", $entry); + if (count($params) != 2) { + continue; + } + $envData[strtolower(trim($params[0]))] = trim($params[1]); + } + $envDataObject = new EntityDataObject( + self::ENV_DATA_OBJECT_NAME, + 'environment', + $envData, + null, + null + ); + $this->data[$envDataObject->getName()] = $envDataObject; + } + } + + /** + * Parses array output of parses into EntityDataObjects. + * @return void + */ + private function parseDataEntities() + { + $entities = $this->arrayData; + + foreach ($entities[self::ENTITY_DATA] as $entityName => $entity) { + $entityType = $entity[self::ENTITY_DATA_TYPE]; + + $dataValues = []; + $linkedEntities = []; + $arrayValues = []; + $uniquenessValues = []; + + if (array_key_exists(self::DATA_VALUES, $entity)) { + foreach ($entity[self::DATA_VALUES] as $dataElement) { + $dataElementKey = strtolower($dataElement[self::DATA_ELEMENT_KEY]); + $dataElementValue = $dataElement[self::DATA_ELEMENT_VALUE]; + if (array_key_exists(self::DATA_ELEMENT_UNIQUENESS_ATTR, $dataElement)) { + $uniquenessValues[$dataElementKey] = $dataElement[self::DATA_ELEMENT_UNIQUENESS_ATTR]; + } + + $dataValues[$dataElementKey] = $dataElementValue; + } + unset($dataElement); + } + + if (array_key_exists(self::REQUIRED_ENTITY, $entity)) { + foreach ($entity[self::REQUIRED_ENTITY] as $linkedEntity) { + $linkedEntityName = $linkedEntity[self::REQUIRED_ENTITY_VALUE]; + $linkedEntityType = $linkedEntity[self::REQUIRED_ENTITY_TYPE]; + + $linkedEntities[$linkedEntityName] = $linkedEntityType; + } + unset($linkedEntity); + } + + if (array_key_exists(self::ARRAY_VALUES, $entity)) { + foreach ($entity[self::ARRAY_VALUES] as $arrayElement) { + $arrayKey = $arrayElement[self::ARRAY_ELEMENT_KEY]; + foreach ($arrayElement[self::ARRAY_ELEMENT_ITEM] as $arrayValue) { + $arrayValues[] = $arrayValue[self::ARRAY_ELEMENT_ITEM_VALUE]; + } + + $dataValues[$arrayKey] = $arrayValues; + } + } + + $entityDataObject = new EntityDataObject( + $entityName, + $entityType, + $dataValues, + $linkedEntities, + $uniquenessValues + ); + + $this->data[$entityDataObject->getName()] = $entityDataObject; + + } + unset($entityName); + unset($entity); + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/JsonDefinitionObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/JsonDefinitionObjectHandler.php new file mode 100644 index 000000000..165962409 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/JsonDefinitionObjectHandler.php @@ -0,0 +1,213 @@ +initJsonDefinitions(); + } + + return self::$JSON_DEFINITION_OBJECT_HANDLER; + } + + /** + * Returns a JsonDefinition object based on name + * + * @param string $jsonDefinitionName + * @return JsonDefinition + */ + public function getObject($jsonDefinitionName) + { + return $this->jsonDefinitions[$jsonDefinitionName]; + } + + /** + * Returns all Json Definition objects + * + * @return array + */ + public function getAllObjects() + { + return $this->jsonDefinitions; + } + + /** + * JsonDefintionArrayProcessor constructor. + */ + private function __construct() + { + $this->jsonDefExtractor = new JsonObjectExtractor(); + } + + /** + * This method takes an operation such as create and a data type such as 'customer' and returns the corresponding + * json definition defined in metadata.xml + * + * @param string $operation + * @param string $dataType + * @return JsonDefinition + */ + public function getJsonDefinition($operation, $dataType) + { + return $this->getObject($operation . $dataType); + } + + /** + * This method reads all jsonDefinitions from metadata xml into memory. + * @return void + */ + private function initJsonDefinitions() + { + $objectManager = ObjectManagerFactory::getObjectManager(); + $metadataParser = $objectManager->create(OperationMetadataParser::class); + foreach ($metadataParser->readOperationMetadata()[JsonDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG] as + $jsonDefName => $jsonDefArray) { + $operation = $jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_TYPE]; + $dataType = $jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE]; + $url = $jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_URL] ?? null; + $method = $jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_METHOD] ?? null; + $auth = $jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_AUTH] ?? null; + $headers = []; + $params = []; + $jsonMetadata = []; + + if (array_key_exists(JsonDefinitionObjectHandler::ENTITY_OPERATION_HEADER, $jsonDefArray)) { + foreach ($jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_HEADER] as $headerEntry) { + $headers[] = $headerEntry[JsonDefinitionObjectHandler::ENTITY_OPERATION_HEADER_PARAM] . ': ' . + $headerEntry[JsonDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE]; + } + } + + if (array_key_exists(JsonDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM, $jsonDefArray)) { + foreach ($jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM] as $paramEntry) { + $params[$paramEntry[JsonDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_TYPE]] + [$paramEntry[JsonDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_KEY]] = + $paramEntry[JsonDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_VALUE]; + } + } + + // extract relevant jsonObjects as JsonElements + if (array_key_exists(JsonDefinitionObjectHandler::ENTITY_OPERATION_JSON_OBJECT, $jsonDefArray)) { + foreach ($jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_JSON_OBJECT] as $jsonObjectArray) { + $jsonMetadata[] = $this->jsonDefExtractor->extractJsonObject($jsonObjectArray); + } + } + + //handle loose entries + + if (array_key_exists(JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY, $jsonDefArray)) { + foreach ($jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY] as $jsonEntryType) { + $jsonMetadata[] = new JsonElement( + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY], + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE], + JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY + ); + } + } + + if (array_key_exists(JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY, $jsonDefArray)) { + foreach ($jsonDefArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY] as $jsonEntryType) { + $jsonSubMetadata = []; + $value = null; + $type = null; + + if (array_key_exists('jsonObject', $jsonEntryType)) { + $jsonNestedElement = $this->jsonDefExtractor->extractJsonObject( + $jsonEntryType['jsonObject'][0] + ); + $jsonSubMetadata[$jsonNestedElement->getKey()] = $jsonNestedElement; + $value = $jsonNestedElement->getValue(); + $type = $jsonNestedElement->getKey(); + } else { + $value = $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_VALUE][0] + [JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_VALUE]; + $type = [JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_VALUE]; + } + + $jsonMetadata[] = new JsonElement( + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_KEY], + $value, + $type, + $jsonSubMetadata + ); + } + } + + $this->jsonDefinitions[$operation . $dataType] = new JsonDefinition( + $jsonDefName, + $operation, + $dataType, + $method, + $url, + $auth, + $headers, + $params, + $jsonMetadata + ); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php new file mode 100644 index 000000000..db602ad07 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php @@ -0,0 +1,259 @@ +name = $entityName; + $this->type = $entityType; + $this->data = $data; + $this->linkedEntities = $linkedEntities; + if ($uniquenessData) { + $this->uniquenessData = $uniquenessData; + } + } + + /** + * Getter for linked entity names + * + * @return array + */ + public function getLinkedEntities() + { + return $this->linkedEntities; + } + + /** + * Getter for entity name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Getter for entity type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Getter for Entity's data. + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * This function retrieves data from an entity defined in xml. + * + * @param string $dataName + * @param int $uniDataFormat + * @return string|null + * @throws TestFrameworkException + */ + public function getDataByName($dataName, $uniDataFormat) + { + if (!$this->isValidUniqueDataFormat($uniDataFormat)) { + throw new TestFrameworkException( + sprintf('Invalid unique data format value: %s \n', $uniDataFormat) + ); + } + + $name = strtolower($dataName); + + if ($this->data !== null && array_key_exists($name, $this->data)) { + $uniData = $this->getUniquenessDataByName($dataName); + if (null === $uniData || $uniDataFormat == self::NO_UNIQUE_PROCESS) { + return $this->data[$name]; + } + + switch ($uniDataFormat) { + case self::SUITE_UNIQUE_VALUE: + if (!function_exists(self::SUITE_UNIQUE_FUNCTION)) { + throw new TestFrameworkException( + sprintf( + 'Unique data format value: %s can only be used when running cests.\n', + $uniDataFormat + ) + ); + } elseif ($uniData == 'prefix') { + return msqs($this->getName()) . $this->data[$name]; + } else { // $uniData == 'suffix' + return $this->data[$name] . msqs($this->getName()); + } + break; + case self::CEST_UNIQUE_VALUE: + if (!function_exists(self::CEST_UNIQUE_FUNCTION)) { + throw new TestFrameworkException( + sprintf( + 'Unique data format value: %s can only be used when running cests.\n', + $uniDataFormat + ) + ); + } elseif ($uniData == 'prefix') { + return msq($this->getName()) . $this->data[$name]; + } else { // $uniData == 'suffix' + return $this->data[$name] . msq($this->getName()); + } + break; + case self::SUITE_UNIQUE_NOTATION: + if ($uniData == 'prefix') { + return self::SUITE_UNIQUE_FUNCTION . '("' . $this->getName() . '")' . $this->data[$name]; + } else { // $uniData == 'suffix' + return $this->data[$name] . self::SUITE_UNIQUE_FUNCTION . '("' . $this->getName() . '")'; + } + break; + case self::CEST_UNIQUE_NOTATION: + if ($uniData == 'prefix') { + return self::CEST_UNIQUE_FUNCTION . '("' . $this->getName() . '")' . $this->data[$name]; + } else { // $uniData == 'suffix' + return $this->data[$name] . self::CEST_UNIQUE_FUNCTION . '("' . $this->getName() . '")'; + } + break; + default: + break; + } + } + + return null; + } + + /** + * This function takes an array of entityTypes indexed by name and a string that represents the type of interest. + * The function returns an array of entityNames relevant to the specified type. + * + * @param string $fieldType + * @return array + */ + public function getLinkedEntitiesOfType($fieldType) + { + $groupedArray = []; + + foreach ($this->linkedEntities as $entityName => $entityType) { + if ($entityType == $fieldType) { + $groupedArray[] = $entityName; + } + } + + return $groupedArray; + } + + /** + * This function retrieves uniqueness data by its name. + * + * @param string $dataName + * @return string|null + */ + public function getUniquenessDataByName($dataName) + { + $name = strtolower($dataName); + + if (array_key_exists($name, $this->uniquenessData)) { + return $this->uniquenessData[$name]; + } + + return null; + } + + /** + * This function retrieves uniqueness data. + * + * @return array|null + */ + public function getUniquenessData() + { + return $this->uniquenessData; + } + + /** + * Validate if input value is a valid unique data format. + * + * @param int $uniDataFormat + * @return bool + */ + private function isValidUniqueDataFormat($uniDataFormat) + { + return in_array( + $uniDataFormat, + [ + self::NO_UNIQUE_PROCESS, + self::SUITE_UNIQUE_VALUE, + self::CEST_UNIQUE_VALUE, + self::SUITE_UNIQUE_NOTATION, + self::CEST_UNIQUE_NOTATION + ], + true + ); + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonDefinition.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonDefinition.php new file mode 100644 index 000000000..f19a2e51e --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonDefinition.php @@ -0,0 +1,242 @@ +name = $name; + $this->operation = $operation; + $this->dataType = $dataType; + $this->apiMethod = $apiMethod; + $this->baseUrl = $apiUrl; + $this->auth = $auth; + $this->headers = $headers; + $this->params = $params; + $this->jsonMetadata = $jsonMetadata; + } + + /** + * Getter for json data type + * + * @return string + */ + public function getDataType() + { + return $this->dataType; + } + + /** + * Getter for json operation + * + * @return string + */ + public function getOperation() + { + return $this->operation; + } + + /** + * Getter for api method + * + * @return string + */ + public function getApiMethod() + { + return $this->apiMethod; + } + + /** + * Getter for api url + * + * @return string + */ + public function getApiUrl() + { + $this->cleanApiUrl(); + + if (array_key_exists('path', $this->params)) { + $this->addPathParam(); + } + + if (array_key_exists('query', $this->params)) { + $this->addQueryParams(); + } + + return $this->apiUrl; + } + + /** + * Getter for auth path + * + * @return string + */ + public function getAuth() + { + return $this->auth; + } + + /** + * Getter for request headers + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Getter for json metadata + * + * @return array + */ + public function getJsonMetadata() + { + return $this->jsonMetadata; + } + + /** + * Function to validate api format and add "/" char where necessary + * + * @return void + */ + private function cleanApiUrl() + { + if (substr($this->baseUrl, -1) == "/") { + $this->apiUrl = rtrim($this->baseUrl, "/"); + } else { + $this->apiUrl = $this->baseUrl; + } + } + + /** + * Function to append path params where necessary + * + * @return void + */ + private function addPathParam() + { + foreach ($this->params['path'] as $paramName => $paramValue) { + $this->apiUrl = $this->apiUrl . "/" . $paramValue; + } + } + + /** + * Function to append query params where necessary + * + * @return void + */ + private function addQueryParams() + { + + foreach ($this->params['query'] as $paramName => $paramValue) { + if (!stringContains("?", $this->apiUrl)) { + $this->apiUrl = $this->apiUrl . "?"; + } else { + $this->apiUrl = $this->apiUrl . "&"; + } + + $this->apiUrl = $paramName . "=" . $paramValue; + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonElement.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonElement.php new file mode 100644 index 000000000..03f0192cc --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonElement.php @@ -0,0 +1,117 @@ +key = $key; + $this->value = $value; + $this->type = $type; + $this->nestedElements = $nestedElements; + $this->nestedMetadata = $nestedMetadata; + } + + /** + * Getter for json parameter name + * + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * Getter for parameter metadata value + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Getter for parameter value type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Returns the nested json element based on the type of entity passed + * + * @param string $type + * @return array + */ + public function getNestedJsonElement($type) + { + if (array_key_exists($type, $this->nestedElements)) { + return $this->nestedElements[$type]; + } + + return []; + } + + /** + * Returns relevant nested json metadata for a json element which is a json object + * + * @return array|null + */ + public function getNestedMetadata() + { + return $this->nestedMetadata; + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Parsers/DataProfileSchemaParser.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Parsers/DataProfileSchemaParser.php new file mode 100644 index 000000000..c9ac3dda2 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Parsers/DataProfileSchemaParser.php @@ -0,0 +1,34 @@ +dataProfiles = $dataProfiles; + } + + /** + * Function to return data as array from data.xml files + * + * @return array + */ + public function readDataProfiles() + { + return $this->dataProfiles->get(); + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Parsers/OperationMetadataParser.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Parsers/OperationMetadataParser.php new file mode 100644 index 000000000..b3c6cc514 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Parsers/OperationMetadataParser.php @@ -0,0 +1,34 @@ +metadata = $metadata; + } + + /** + * Returns an array containing all data read from operations.xml files. + * + * @return array + */ + public function readOperationMetadata() + { + return $this->metadata->get(); + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/JsonObjectExtractor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/JsonObjectExtractor.php new file mode 100644 index 000000000..6f30192b9 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/JsonObjectExtractor.php @@ -0,0 +1,129 @@ +extractJsonEntries($jsonMetadata, $jsonObjectArray[JsonObjectExtractor::JSON_OBJECT_ENTRY]); + } + + // extract nested arrays + if (array_key_exists(JsonObjectExtractor::JSON_OBJECT_ARRAY, $jsonObjectArray)) { + $this->extractJsonArrays($jsonMetadata, $jsonObjectArray[JsonObjectExtractor::JSON_OBJECT_ARRAY]); + } + + // extract nested + if (array_key_exists(JsonObjectExtractor::JSON_OBJECT_OBJ_NAME, $jsonObjectArray)) { + foreach ($jsonObjectArray[JsonObjectExtractor::JSON_OBJECT_OBJ_NAME] as $jsonObject) { + $nestedJsonElement = $this->extractJsonObject($jsonObject); + $nestedJsonElements[$nestedJsonElement->getKey()] = $nestedJsonElement; + } + } + + // a jsonObject specified in xml must contain corresponding metadata for the object + if (empty($jsonMetadata)) { + throw new \Exception("must specificy jsonObject metadata if declaration is used"); + } + + return new JsonElement( + $jsonDefKey, + $dataType, + JsonObjectExtractor::JSON_OBJECT_OBJ_NAME, + $nestedJsonElements, + $jsonMetadata + ); + } + + /** + * Creates and Adds relevant JsonElements from json entries defined within jsonObject array + * + * @param array &$jsonMetadata + * @param array $jsonEntryArray + * @return void + */ + private function extractJsonEntries(&$jsonMetadata, $jsonEntryArray) + { + foreach ($jsonEntryArray as $jsonEntryType) { + $jsonMetadata[] = new JsonElement( + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY], + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE], + JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY + ); + } + } + + /** + * Creates and Adds relevant JsonElements from json arrays defined within jsonObject array + * + * @param array &$jsonArrayData + * @param array $jsonArrayArray + * @return void + */ + private function extractJsonArrays(&$jsonArrayData, $jsonArrayArray) + { + foreach ($jsonArrayArray as $jsonEntryType) { + $jsonElementValue = + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_VALUE][0] + [JsonObjectExtractor::JSON_OBJECT_ARRAY_VALUE] ?? null; + + $nestedJsonElements = []; + if (array_key_exists(JsonObjectExtractor::JSON_OBJECT_OBJ_NAME, $jsonEntryType)) { + //add the key to reference this object later + $jsonObjectKeyedArray = $jsonEntryType[JsonObjectExtractor::JSON_OBJECT_OBJ_NAME][0]; + $jsonObjectKeyedArray[JsonObjectExtractor::JSON_OBJECT_KEY] = + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_KEY]; + $jsonElement = $this->extractJsonObject($jsonObjectKeyedArray); + $jsonElementValue = $jsonElement->getValue(); + $nestedJsonElements[$jsonElement->getValue()] = $jsonElement; + } + $jsonArrayData[] = new JsonElement( + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_KEY], + $jsonElementValue, + JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY, + $nestedJsonElements + ); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd new file mode 100644 index 000000000..52e144f22 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd new file mode 100644 index 000000000..24a7eed6b --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd @@ -0,0 +1,120 @@ + + + + + + + The root element for configuration data. + + + + + + + + An element that contains configuration array containing all Entity elements. + + + + + + + + + + + + + Element containing Data/Value pair. + + + + + + + Element containing required entity to this parent entity. + + + + + + + Element that contains a reference to non singular data/values. + + + + + + + + Name of the Entity. + + + + + + + Node containing the exact name of Entity type. Used later to find specific Persistence Layer Model + class. + + + + + + + + + + + + Key attribute of data/value pair. + + + + + + + Add suite or test wide unique sequence as "prefix" or "suffix" to the data value if specified. + + + + + + + + + + + + + Individual piece of data to be passed in as part of the parrent array type. + + + + + + + + + + + + + + Type attribute of required entity. + + + + + + + + + + + + + + + diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/sample.xml b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/sample.xml new file mode 100644 index 000000000..722e5e6e7 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/sample.xml @@ -0,0 +1,25 @@ + + + + + simpleConfiguration + regressionConfiguration + + asdf + + + + AssertNumberOne + AssertNumberTwo + + + FirstNameData + + + AssertNumberOne + AssertNumberTwo + AssertNumberThree + + + \ No newline at end of file diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/TestFrameworkException.php b/src/Magento/FunctionalTestingFramework/Exceptions/TestFrameworkException.php new file mode 100644 index 000000000..9374feebe --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Exceptions/TestFrameworkException.php @@ -0,0 +1,22 @@ +getModule(MagentoWebDriver::class)->_reconfigure([$config => $value]); + } + + /** + * Get WebDriver configuration. + * + * @param string $config + * @return string + */ + public function getConfiguration($config) + { + return $this->getModule(MagentoWebDriver::class)->_getConfig($config); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Helper/AdminUrlList.php b/src/Magento/FunctionalTestingFramework/Helper/AdminUrlList.php new file mode 100644 index 000000000..edb2b051b --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Helper/AdminUrlList.php @@ -0,0 +1,188 @@ + 'application/json']; + + /** + * Rest API client. + * + * @var Client + */ + private $guzzle_client; + + /** + * EntityRESTApiHelper constructor. + * @param string $host + * @param string $port + */ + public function __construct($host, $port) + { + $this->guzzle_client = new Client([ + 'base_uri' => "http://${host}:${port}", + 'timeout' => 5.0, + ]); + } + + /** + * Submit Auth API Request. + * + * @param string $apiMethod + * @param string $requestURI + * @param string $jsonBody + * @param array $headers + * @return \Psr\Http\Message\ResponseInterface + */ + public function submitAuthAPIRequest($apiMethod, $requestURI, $jsonBody, $headers) + { + $allHeaders = $headers; + $authTokenVal = $this->getAuthToken(); + $authToken = ['Authorization' => 'Bearer ' . $authTokenVal]; + $allHeaders = array_merge($allHeaders, $authToken); + + return $this->submitAPIRequest($apiMethod, $requestURI, $jsonBody, $allHeaders); + } + + /** + * Function that sends a REST call to the integration endpoint for an authorization token. + * + * @return string + */ + private function getAuthToken() + { + $jsonArray = json_encode(['username' => 'admin', 'password' => 'admin123']); + + $response = $this->submitAPIRequest( + 'POST', + self::INTEGRATION_ADMIN_TOKEN_URI, + $jsonArray, + self::APPLICATION_JSON_HEADER + ); + + if ($response->getStatusCode() != 200) { + throwException($response->getReasonPhrase() .' Could not get admin token from service, please check logs.'); + } + + $authToken = str_replace('"', "", $response->getBody()->getContents()); + return $authToken; + } + + /** + * Function that submits an api request from the guzzle client using the following parameters: + * + * @param string $apiMethod + * @param string $requestURI + * @param string $jsonBody + * @param array $headers + * @return \Psr\Http\Message\ResponseInterface + */ + private function submitAPIRequest($apiMethod, $requestURI, $jsonBody, $headers) + { + $response = $this->guzzle_client->request( + $apiMethod, + $requestURI, + [ + 'headers' => $headers, + 'body' => $jsonBody + ] + ); + + return $response; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Helper/MagentoFakerData.php b/src/Magento/FunctionalTestingFramework/Helper/MagentoFakerData.php new file mode 100644 index 000000000..9cf5322b2 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Helper/MagentoFakerData.php @@ -0,0 +1,124 @@ + $faker->title, + 'firstname' => $faker->firstName, + 'middlename' => $faker->firstName, + 'lastname' => $faker->lastName, + 'suffix' => \Faker\Provider\en_US\Person::suffix(), + 'email' => $faker->email, + 'dateOfBirth' => $faker->date($format = 'm/d/Y', $max = 'now'), + 'gender' => rand(0, 1), + 'group_id' => 1, + 'store_id' => 1, + 'website_id' => 1, + 'taxVatNumber' => \Faker\Provider\at_AT\Payment::vat(), + 'company' => $faker->company, + 'phoneNumber' => $faker->phoneNumber, + 'address' => [ + 'address1' => $faker->streetAddress, + 'address2' => $faker->streetAddress, + 'city' => $faker->city, + 'country' => 'United States', + 'state' => \Faker\Provider\en_US\Address::state(), + 'zipCode' => $faker->postcode + ] + ]; + return array_merge($customerData, $additional); + } + + /** + * Get category data. + * + * @return array + */ + public function getCategoryData() + { + $faker = \Faker\Factory::create(); + + return [ + 'enableCategory' => $faker->boolean(), + 'includeInMenu' => $faker->boolean(), + 'categoryName' => $faker->md5, + 'categoryImage' => '', + 'description' => $faker->sentence($nbWords = 10, $variableNbWords = true), + 'addCMSBlock' => '', + + 'urlKey' => $faker->uuid, + 'metaTitle' => $faker->word, + 'metaKeywords' => $faker->sentence($nbWords = 5, $variableNbWords = true), + 'metaDescription' => $faker->sentence($nbWords = 10, $variableNbWords = true), + ]; + } + + /** + * Get simple product data. + * + * @param integer $categoryId + * @param array $productData + * @return array + */ + public function getProductData($categoryId = 0, $productData = []) + { + $faker = \Faker\Factory::create(); + return [ + 'enableProduct' => $faker->boolean(), + 'attributeSet' => '', + 'productName' => $faker->text($maxNbChars = 20), + 'sku' => \Faker\Provider\DateTime::unixTime($max = 'now'), + 'price' => $faker->randomFloat($nbMaxDecimals = 2, $min = 0, $max = 999), + 'quantity' => $faker->numberBetween($min = 1, $max = 999), + + 'urlKey' => $faker->uuid, + 'metaTitle' => $faker->word, + 'metaKeywords' => $faker->sentence($nbWords = 5, $variableNbWords = true), + 'metaDescription' => $faker->sentence($nbWords = 10, $variableNbWords = true) + ]; + } + + /** + * Get Content Page Data. + * + * @return array + */ + public function getContentPage() + { + $faker = \Faker\Factory::create(); + + $pageContent = [ + 'pageTitle' => $faker->sentence($nbWords = 3, $variableNbWords = true), + 'contentHeading' => $faker->sentence($nbWords = 3, $variableNbWords = true), + 'contentBody' => $faker->sentence($nbWords = 10, $variableNbWords = true), + 'urlKey' => $faker->uuid, + 'metaTitle' => $faker->word, + 'metaKeywords' => $faker->sentence($nbWords = 5, $variableNbWords = true), + 'metaDescription' => $faker->sentence($nbWords = 10, $variableNbWords = true), + 'from' => $faker->date($format = 'm/d/Y', $max = 'now'), + 'to' => $faker->date($format = 'm/d/Y') + ]; + $pageContent['layoutUpdateXml'] = "ToveJaniReminder"; + $pageContent['layoutUpdateXml'] .= "Don't forget me this weekend!"; + + return $pageContent; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php new file mode 100644 index 000000000..bae3c50f7 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php @@ -0,0 +1,665 @@ + '', + 'username' => '', + 'password' => '' + ]; + + /** + * Admin tokens for Magento webapi access. + * + * @var array + */ + protected static $adminTokens = []; + + /** + * Before suite. + * + * @param array $settings + */ + public function _beforeSuite($settings = []) + { + parent::_beforeSuite($settings); + $this->haveHttpHeader('Content-Type', 'application/json'); + $this->sendPOST( + 'integration/admin/token', + ['username' => $this->config['username'], 'password' => $this->config['password']] + ); + $token = substr($this->grabResponse(), 1, strlen($this->grabResponse())-2); + $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); + $this->haveHttpHeader('Authorization', 'Bearer ' . $token); + self::$adminTokens[$this->config['username']] = $token; + $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoSequence')->_initialize(); + } + + /** + * After suite. + */ + public function _afterSuite() + { + parent::_afterSuite(); + $this->deleteHeader('Authorization'); + } + + /** + * Get admin auth token by username and password. + * + * @param string $username + * @param string $password + * @param bool $newToken + * @return string + * @part json + * @part xml + */ + public function getAdminAuthToken($username = null, $password = null, $newToken = false) + { + $username = !is_null($username) ? $username : $this->config['username']; + $password = !is_null($password) ? $password : $this->config['password']; + + // Use existing token if it exists + if (!$newToken + && (isset(self::$adminTokens[$username]) || array_key_exists($username, self::$adminTokens))) { + return self::$adminTokens[$username]; + } + $this->haveHttpHeader('Content-Type', 'application/json'); + $this->sendPOST('integration/admin/token', ['username' => $username, 'password' => $password]); + $token = substr($this->grabResponse(), 1, strlen($this->grabResponse())-2); + $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); + self::$adminTokens[$username] = $token; + return $token; + } + + /** + * Admin token authentication for a given user. + * + * @param string $username + * @param string $password + * @param bool $newToken + * @part json + * @part xml + */ + public function amAdminTokenAuthenticated($username = null, $password = null, $newToken = false) + { + $username = !is_null($username) ? $username : $this->config['username']; + $password = !is_null($password) ? $password : $this->config['password']; + + $this->haveHttpHeader('Content-Type', 'application/json'); + if ($newToken || !isset(self::$adminTokens[$username])) { + $this->sendPOST('integration/admin/token', ['username' => $username, 'password' => $password]); + $token = substr($this->grabResponse(), 1, strlen($this->grabResponse()) - 2); + $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); + self::$adminTokens[$username] = $token; + } + $this->amBearerAuthenticated(self::$adminTokens[$username]); + } + + /** + * Send REST API request. + * + * @param string $endpoint + * @param string $httpMethod + * @param array $params + * @param string $grabByJsonPath + * @param bool $decode + * @return mixed + * @throws \LogicException + * @part json + * @part xml + */ + public function sendRestRequest($endpoint, $httpMethod, $params = [], $grabByJsonPath = null, $decode = true) + { + $this->amAdminTokenAuthenticated(); + switch ($httpMethod) { + case self::HTTP_METHOD_GET: + $this->sendGET($endpoint, $params); + break; + case self::HTTP_METHOD_POST: + $this->sendPOST($endpoint, $params); + break; + case self::HTTP_METHOD_PUT: + $this->sendPUT($endpoint, $params); + break; + case self::HTTP_METHOD_DELETE: + $this->sendDELETE($endpoint, $params); + break; + default: + throw new \LogicException("HTTP method '{$httpMethod}' is not supported."); + } + $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); + + if (!$decode && is_null($grabByJsonPath)) { + return $this->grabResponse(); + } elseif (!$decode) { + return $this->grabDataFromResponseByJsonPath($grabByJsonPath); + } else { + return \GuzzleHttp\json_decode($this->grabResponse()); + } + } + + /** + * Create a category in Magento. + * + * @param array $categoryData + * @return array|mixed + * @part json + * @part xml + */ + public function requireCategory($categoryData = []) + { + if (!$categoryData) { + $categoryData = $this->getCategoryApiData(); + } + $categoryData = $this->sendRestRequest( + self::$categoryEndpoint, + self::HTTP_METHOD_POST, + ['category' => $categoryData] + ); + return $categoryData; + } + + /** + * Create a simple product in Magento. + * + * @param int $categoryId + * @param array $simpleProductData + * @return array|mixed + * @part json + * @part xml + */ + public function requireSimpleProduct($categoryId = 0, $simpleProductData = []) + { + if (!$simpleProductData) { + $simpleProductData = $this->getProductApiData('simple', $categoryId); + } + $simpleProductData = $this->sendRestRequest( + self::$productEndpoint, + self::HTTP_METHOD_POST, + ['product' => $simpleProductData] + ); + return $simpleProductData; + } + + /** + * Create a configurable product in Magento. + * + * @param int $categoryId + * @param array $configurableProductData + * @return array|mixed + * @part json + * @part xml + */ + public function requireConfigurableProduct($categoryId = 0, $configurableProductData = []) + { + $configurableProductData = $this->getProductApiData('configurable', $categoryId, $configurableProductData); + $this->sendRestRequest( + self::$productEndpoint, + self::HTTP_METHOD_POST, + ['product' => $configurableProductData] + ); + + $attributeData = $this->getProductAttributeApiData(); + $attribute = $this->sendRestRequest( + self::$productAttributesEndpoint, + self::HTTP_METHOD_POST, + $attributeData + ); + $options = $this->sendRestRequest( + sprintf(self::$productAttributesOptionsEndpoint, $attribute->attribute_code), + self::HTTP_METHOD_GET + ); + + $attributeSetData = $this->getAssignAttributeToAttributeSetApiData($attribute->attribute_code); + $this->sendRestRequest( + self::$productAttributeSetEndpoint, + self::HTTP_METHOD_POST, + $attributeSetData + ); + + $simpleProduct1Data = $this->getProductApiData('simple', $categoryId); + array_push( + $simpleProduct1Data['custom_attributes'], + [ + 'attribute_code' => $attribute->attribute_code, + 'value' => $options[1]->value + ] + ); + $simpleProduct1Id = $this->sendRestRequest( + self::$productEndpoint, + self::HTTP_METHOD_POST, + ['product' => $simpleProduct1Data] + )->id; + + $simpleProduct2Data = $this->getProductApiData('simple', $categoryId); + array_push( + $simpleProduct2Data['custom_attributes'], + [ + 'attribute_code' => $attribute->attribute_code, + 'value' => $options[2]->value + ] + ); + $simpleProduct2Id = $this->sendRestRequest( + self::$productEndpoint, + self::HTTP_METHOD_POST, + ['product' => $simpleProduct2Data] + )->id; + + $tAttributes[] = [ + 'id' => $attribute->attribute_id, + 'code' => $attribute->attribute_code + ]; + + $tOptions = [ + intval($options[1]->value), + intval($options[2]->value) + ]; + + $configOptions = $this->getConfigurableProductOptionsApiData($tAttributes, $tOptions); + + $configurableProductData = $this->getConfigurableProductApiData( + $configOptions, + [$simpleProduct1Id, $simpleProduct2Id], + $configurableProductData + ); + + $configurableProductData = $this->sendRestRequest( + self::$productEndpoint . '/' . $configurableProductData['sku'], + self::HTTP_METHOD_PUT, + ['product' => $configurableProductData] + ); + return $configurableProductData; + } + + /** + * Create a product attribute in Magento. + * + * @param string $code + * @return array|mixed + * @part json + * @part xml + */ + public function requireProductAttribute($code = 'attribute') + { + $attributeData = $this->getProductAttributeApiData($code); + $attributeData = $this->sendRestRequest( + self::$productAttributesEndpoint, + self::HTTP_METHOD_POST, + $attributeData + ); + return $attributeData; + } + + /** + * Create a customer in Magento. + * + * @param array $customerData + * @param string $password + * @return array|mixed + * @part json + * @part xml + */ + public function requireCustomer(array $customerData = [], $password = '123123qW') + { + if (!$customerData) { + $customerData = $this->getCustomerApiData(); + } + $customerData = $this->getCustomerApiDataWithPassword($customerData, $password); + $customerData = $this->sendRestRequest( + self::$customersEndpoint, + self::HTTP_METHOD_POST, + $customerData + ); + return $customerData; + } + + /** + * Get category api data. + * + * @param array $categoryData + * @return array + * @part json + * @part xml + */ + public function getCategoryApiData($categoryData = []) + { + $faker = \Faker\Factory::create(); + $sq = sqs(); + return array_replace_recursive( + [ + 'parent_id' => '2', + 'name' => 'category' . $sq, + 'is_active' => true, + 'include_in_menu' => true, + 'available_sort_by' => ['position', 'name'], + 'custom_attributes' => [ + ['attribute_code' => 'url_key', 'value' => 'category' . $sq], + ['attribute_code' => 'description', 'value' => $faker->text(20)], + ['attribute_code' => 'meta_title', 'value' => $faker->text(20)], + ['attribute_code' => 'meta_keywords', 'value' => $faker->text(20)], + ['attribute_code' => 'meta_description', 'value' => $faker->text(20)], + ['attribute_code' => 'display_mode', 'value' => 'PRODUCTS'], + ['attribute_code' => 'landing_page', 'value' => ''], + ['attribute_code' => 'is_anchor', 'value' => '0'], + ['attribute_code' => 'custom_use_parent_settings', 'value' => '0'], + ['attribute_code' => 'custom_apply_to_products', 'value' => '0'], + ['attribute_code' => 'custom_design', 'value' => ''], + ['attribute_code' => 'page_layout', 'value' => ''], + ['attribute_code' => 'custom_design_to', 'value' => $faker->date($format = 'm/d/Y')], + ['attribute_code' => 'custom_design_from', 'value' => $faker->date($format = 'm/d/Y', $max = 'now')] + ] + ], + $categoryData + ); + } + + /** + * Get simple product api data. + * + * @param string $type + * @param integer $categoryId + * @param array $productData + * @return array + * @part json + * @part xml + */ + public function getProductApiData($type = 'simple', $categoryId = 0, $productData = []) + { + $faker = \Faker\Factory::create(); + $sq = sqs(); + return array_replace_recursive( + [ + 'sku' => $type . '_product_sku' . $sq, + 'name' => $type . '_product' . $sq, + 'visibility' => 4, + 'type_id' => $type, + 'price' => $faker->randomFloat(2, 1), + 'status' => 1, + 'attribute_set_id' => 4, + 'extension_attributes' => [ + 'stock_item' => ['is_in_stock' => 1, 'qty' => $faker->numberBetween(100, 9000)] + ], + 'custom_attributes' => [ + ['attribute_code' => 'url_key', 'value' => $type . '_product' . $sq], + ['attribute_code' => 'tax_class_id', 'value' => 2], + ['attribute_code' => 'category_ids', 'value' => $categoryId], + ], + ], + $productData + ); + } + + /** + * Get Customer Api data. + * + * @param array $customerData + * @return array + * @part json + * @part xml + */ + public function getCustomerApiData($customerData = []) + { + $faker = \Faker\Factory::create(); + return array_replace_recursive( + [ + 'firstname' => $faker->firstName, + 'middlename' => $faker->firstName, + 'lastname' => $faker->lastName, + 'email' => $faker->email, + 'gender' => rand(0, 1), + 'group_id' => 1, + 'store_id' => 1, + 'website_id' => 1, + 'custom_attributes' => [ + [ + 'attribute_code' => 'disable_auto_group_change', + 'value' => '0', + ], + ], + ], + $customerData + ); + } + + /** + * Get customer data including password. + * + * @param array $customerData + * @param string $password + * @return array + * @part json + * @part xml + */ + public function getCustomerApiDataWithPassword($customerData = [], $password = '123123qW') + { + return ['customer' => self::getCustomerApiData($customerData), 'password' => $password]; + } + + /** + * @param string $code + * @param array $attributeData + * @return array + * @part json + * @part xml + */ + public function getProductAttributeApiData($code = 'attribute', $attributeData = []) + { + $sq = sqs(); + return array_replace_recursive( + [ + 'attribute' => [ + 'attribute_code' => $code . $sq, + 'frontend_labels' => [ + [ + 'store_id' => 0, + 'label' => $code . $sq + ], + ], + 'is_required' => false, + 'is_unique' => false, + 'is_visible' => true, + 'scope' => 'global', + 'default_value' => '', + 'frontend_input' => 'select', + 'is_visible_on_front' => true, + 'is_searchable' => true, + 'is_visible_in_advanced_search' => true, + 'is_filterable' => true, + 'is_filterable_in_search' => true, + //'is_used_in_grid' => true, + //'is_visible_in_grid' => true, + //'is_filterable_in_grid' => true, + 'used_in_product_listing' => true, + 'is_used_for_promo_rules' => true, + 'options' => [ + [ + 'label' => 'option1', + 'value' => '', + 'sort_order' => 0, + 'is_default' => true, + 'store_labels' => [ + [ + 'store_id' => 0, + 'label' => 'option1' + ], + [ + 'store_id' => 1, + 'label' => 'option1' + ] + ] + ], + [ + 'label' => 'option2', + 'value' => '', + 'sort_order' => 1, + 'is_default' => false, + 'store_labels' => [ + [ + 'store_id' => 0, + 'label' => 'option2' + ], + [ + 'store_id' => 1, + 'label' => 'option2' + ] + ] + ] + ] + ], + ], + $attributeData + ); + } + + /** + * @param array $attributes + * @param array $optionIds + * @return array + * @part json + * @part xml + */ + public function getConfigurableProductOptionsApiData($attributes, $optionIds) + { + $configurableProductOptions = []; + foreach ($attributes as $attribute) { + $attributeItem = [ + 'attribute_id' => (string)$attribute['id'], + 'label' => $attribute['code'], + 'values' => [] + ]; + foreach ($optionIds as $optionId) { + $attributeItem['values'][] = ['value_index' => $optionId]; + } + $configurableProductOptions [] = $attributeItem; + } + return $configurableProductOptions; + } + + /** + * @param array $configurableProductOptions + * @param array $childProductIds + * @param array $configurableProduct + * @param int $categoryId + * @return array + * @part json + * @part xml + */ + public function getConfigurableProductApiData( + array $configurableProductOptions, + array $childProductIds, + array $configurableProduct = [], + int $categoryId = 0 + ) { + if (!$configurableProduct) { + $configurableProduct = $this->getProductApiData('configurable', $categoryId); + } + $configurableProduct = array_merge_recursive( + $configurableProduct, + [ + 'extension_attributes' => [ + 'configurable_product_options' => $configurableProductOptions, + 'configurable_product_links' => $childProductIds, + ], + ] + ); + return $configurableProduct; + } + + /** + * @param $attributeCode + * @param int $attributeSetId + * @param int $attributeGroupId + * @return array + * @part json + * @part xml + */ + public function getAssignAttributeToAttributeSetApiData( + $attributeCode, + int $attributeSetId = 4, + int $attributeGroupId = 7 + ) { + return [ + 'attributeSetId' => $attributeSetId, + 'attributeGroupId' => $attributeGroupId, + 'attributeCode' => $attributeCode, + 'sortOrder' => 0 + ]; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php b/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php new file mode 100644 index 000000000..52a995be1 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php @@ -0,0 +1,21 @@ + '']; +} + +if (!function_exists('msq') && !function_exists('msqs')) { + require_once __DIR__ . '/../Util/msq.php'; +} else { + throw new ModuleException('Magento\FunctionalTestingFramework\Module\MagentoSequence', "function 'msq' and 'msqs' already defined"); +} diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php new file mode 100644 index 000000000..02803846b --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php @@ -0,0 +1,303 @@ + null, + LC_CTYPE => null, + LC_MONETARY => null, + LC_NUMERIC => null, + LC_TIME => null, + LC_MESSAGES => null, + ]; + + /** + * Returns URL of a host. + * + * @api + * @return mixed + * @throws ModuleConfigException + */ + public function _getUrl() + { + if (!isset($this->config['url'])) { + throw new ModuleConfigException( + __CLASS__, + "Module connection failure. The URL for client can't bre retrieved" + ); + } + return $this->config['url']; + } + + /** + * Uri of currently opened page. + * + * @return string + * @api + * @throws ModuleException + */ + public function _getCurrentUri() + { + $url = $this->webDriver->getCurrentURL(); + if ($url == 'about:blank') { + throw new ModuleException($this, 'Current url is blank, no page was opened'); + } + return Uri::retrieveUri($url); + } + + /** + * Returns an array of Elements. + * + * @param string $locator + * @return array + */ + public function findElement($locator) + { + return $this->_findElements($locator); + } + + /** + * Login Magento Admin with given username and password. + * + * @param string $username + * @param string $password + * @return void + */ + public function loginAsAdmin($username = null, $password = null) + { + $this->amOnPage($this->config['backend_name']); + $this->fillField('login[username]', !is_null($username) ? $username : $this->config['username']); + $this->fillField('login[password]', !is_null($password) ? $password : $this->config['password']); + $this->click('Sign in'); + $this->waitForPageLoad(); + + $this->closeAdminNotification(); + } + + /** + * Close admin notification popup windows. + * + * @return void + */ + public function closeAdminNotification() + { + // Cheating here for the minute. Still working on the best method to deal with this issue. + try { + $this->executeJS("jQuery('.modal-popup').remove(); jQuery('.modals-overlay').remove();"); + } catch (\Exception $e) {} + } + + + /** + * Search for and Select multiple options from a Magento Multi-Select drop down menu. + * e.g. The drop down menu you use to assign Products to Categories. + * + * @param $select + * @param array $options + * @param bool $requireAction + */ + public function searchAndMultiSelectOption($select, array $options, $requireAction = false) + { + $selectDropdown = $select . ' .action-select.admin__action-multiselect'; + $selectSearchText = $select + . ' .admin__action-multiselect-search-wrap>input[data-role="advanced-select-text"]'; + $selectSearchResult = $select . ' .admin__action-multiselect-label>span'; + + $this->waitForPageLoad(); + $this->waitForElementVisible($selectDropdown); + $this->click($selectDropdown); + foreach ($options as $option) { + $this->waitForPageLoad(); + $this->fillField($selectSearchText, ''); + $this->waitForPageLoad(); + $this->fillField($selectSearchText, $option); + $this->waitForPageLoad(); + $this->click($selectSearchResult); + } + if ($requireAction) { + $selectAction = $select . ' button[class=action-default]'; + $this->waitForPageLoad(); + $this->click($selectAction); + } + } + + /** + * Wait for all Ajax calls to finish. + * + * @param int $timeout + */ + public function waitForAjaxLoad($timeout = 15) + { + $this->waitForJS('return !!window.jQuery && window.jQuery.active == 0;', $timeout); + $this->wait(1); + } + + /** + * Wait for all JavaScript to finish executing. + * + * @param int $timeout + */ + public function waitForPageLoad($timeout = 15) + { + $this->waitForJS('return document.readyState == "complete"', $timeout); + $this->waitForAjaxLoad($timeout); + } + + /** + * Wait for the Loading mask to disappear. + */ + public function waitForLoadingMaskToDisappear() + { + $this->waitForElementNotVisible(self::$loadingMask, 30); + } + + /** + * Verify that there are no JavaScript errors in the console. + * + * @throws ModuleException + */ + public function dontSeeJsError() + { + $logs = $this->webDriver->manage()->getLog('browser'); + foreach ($logs as $log) { + if ($log['level'] == 'SEVERE') { + throw new ModuleException($this, 'Errors in JavaScript: ' . json_encode($log)); + } + } + } + + /** + * @param float $money + * @param string $locale + * @return array + */ + public function formatMoney(float $money, $locale = 'en_US.UTF-8') + { + $this->mSetLocale(LC_MONETARY, $locale); + $money = money_format('%.2n', $money); + $this->mResetLocale(); + $prefix = substr($money, 0, 1); + $number = substr($money, 1); + return ['prefix' => $prefix, 'number' => $number]; + } + + /** + * Parse float number with thousands_sep. + * + * @param $floatString + * @return float + */ + public function parseFloat($floatString){ + $floatString = str_replace(',', '', $floatString); + return floatval($floatString); + } + + /** + * @param int $category + * @param string $locale + */ + public function mSetLocale(int $category, $locale) + { + if (self::$localeAll[$category] == $locale) { + return; + } + foreach (self::$localeAll as $c => $l) { + self::$localeAll[$c] = setlocale($c, 0); + } + setlocale($category, $locale); + } + + /** + * Reset Locale setting. + */ + public function mResetLocale() + { + foreach (self::$localeAll as $c => $l) { + if (!is_null($l)) { + setlocale($c, $l); + self::$localeAll[$c] = null; + } + } + } + + /** + * Scroll to the Top of the Page. + */ + public function scrollToTopOfPage() + { + $this->executeJS('window.scrollTo(0,0);'); + } + + /** + * Override for _failed method in Codeception method. Adds png and html attachments to allure report + * following parent execution of test failure processing. + * @param TestInterface $test + * @param \Exception $fail + */ + public function _failed(TestInterface $test, $fail) + { + parent::_failed($test, $fail); + + // Reconstruct file naming from codeception method + $filename = preg_replace('~\W~', '.', Descriptor::getTestSignature($test)); + $outputDir = codecept_output_dir(); + $pngReport = $outputDir . mb_strcut($filename, 0, 245, 'utf-8') . '.fail.png'; + $htmlReport = $outputDir . mb_strcut($filename, 0, 244, 'utf-8') . '.fail.html'; + $this->addAttachment($pngReport, $test->getMetadata()->getName() . '.png', 'image/png'); + $this->addAttachment($htmlReport, $test->getMetadata()->getName() . '.html', 'text/html'); + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager.php b/src/Magento/FunctionalTestingFramework/ObjectManager.php new file mode 100644 index 000000000..51b64dd29 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager.php @@ -0,0 +1,124 @@ +sharedInstances[\Magento\FunctionalTestingFramework\ObjectManager::class] = $this; + } + + /** + * Get list of parameters for class method + * + * @param string $type + * @param string $method + * @return array|null + */ + public function getParameters($type, $method) + { + return $this->factory->getParameters($type, $method); + } + + /** + * Resolve and prepare arguments for class method + * + * @param object $object + * @param string $method + * @param array $arguments + * @return array + */ + public function prepareArguments($object, $method, array $arguments = []) + { + return $this->factory->prepareArguments($object, $method, $arguments); + } + + // @codingStandardsIgnoreStart + /** + * Invoke class method with prepared arguments + * + * @param object $object + * @param string $method + * @param array $arguments + * @return mixed + */ + public function invoke($object, $method, array $arguments = []) + { + return $this->factory->invoke($object, $method, $arguments); + } + // @codingStandardsIgnoreEnd + + /** + * Set object manager instance + * + * @param ObjectManager $objectManager + * @return void + */ + public static function setInstance(ObjectManager $objectManager) + { + self::$instance = $objectManager; + } + + /** + * Retrieve object manager + * + * @return ObjectManager|bool + * @throws \RuntimeException + */ + public static function getInstance() + { + if (!self::$instance instanceof ObjectManager) { + return false; + } + return self::$instance; + } + + /** + * Avoid to serialize Closure properties + * + * @return array + */ + public function __sleep() + { + return []; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config.php new file mode 100644 index 000000000..fdc8ca373 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config.php @@ -0,0 +1,54 @@ +nonShared[$type])) { + return false; + } + + if (isset($this->virtualTypes[$type])) { + return true; + } + + if (!isset($this->nonSharedRefClasses[$type])) { + $this->nonSharedRefClasses[$type] = new \ReflectionClass($type); + } + foreach ($this->nonShared as $noneShared => $flag) { + if ($this->nonSharedRefClasses[$type]->isSubclassOf($noneShared)) { + return false; + } + } + + return true; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php new file mode 100644 index 000000000..502dba0c0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php @@ -0,0 +1,246 @@ +relations = $relations ? : new RelationsRuntime(); + $this->definitions = $definitions ? : new DefinitionRuntime(); + } + + /** + * Retrieve list of arguments per type + * + * @param string $type + * @return array + */ + public function getArguments($type) + { + return isset($this->mergedArguments[$type]) + ? $this->mergedArguments[$type] + : $this->collectConfiguration($type); + } + + /** + * Check whether type is shared + * + * @param string $type + * @return bool + */ + public function isShared($type) + { + return !isset($this->nonShared[$type]); + } + + /** + * Retrieve instance type + * + * @param string $instanceName + * @return string + */ + public function getInstanceType($instanceName) + { + while (isset($this->virtualTypes[$instanceName])) { + $instanceName = $this->virtualTypes[$instanceName]; + } + return $instanceName; + } + + /** + * Retrieve preference for type + * + * @param string $type + * @return string + * @throws \LogicException + */ + public function getPreference($type) + { + $type = ltrim($type, '\\'); + $preferencePath = []; + while (isset($this->preferences[$type])) { + if (isset($preferencePath[$this->preferences[$type]])) { + throw new \LogicException( + 'Circular type preference: ' . + $type . + ' relates to ' . + $this->preferences[$type] . + ' and viceversa.' + ); + } + $type = $this->preferences[$type]; + $preferencePath[$type] = 1; + } + return $type; + } + + /** + * Collect parent types configuration for requested type + * + * @param string $type + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function collectConfiguration($type) + { + if (!isset($this->mergedArguments[$type])) { + if (isset($this->virtualTypes[$type])) { + $arguments = $this->collectConfiguration($this->virtualTypes[$type]); + } else { + if ($this->relations->has($type)) { + $relations = $this->relations->getParents($type); + $arguments = []; + foreach ($relations as $relation) { + if ($relation) { + $relationArguments = $this->collectConfiguration($relation); + if ($relationArguments) { + $arguments = array_replace($arguments, $relationArguments); + } + } + } + } else { + $arguments = []; + } + } + + if (isset($this->arguments[$type])) { + if ($arguments && count($arguments)) { + $arguments = array_replace_recursive($arguments, $this->arguments[$type]); + } else { + $arguments = $this->arguments[$type]; + } + } + $this->mergedArguments[$type] = $arguments; + return $arguments; + } + return $this->mergedArguments[$type]; + } + + /** + * Merge configuration + * + * @param array $configuration + * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function mergeConfiguration(array $configuration) + { + foreach ($configuration as $key => $curConfig) { + switch ($key) { + case 'preferences': + foreach ($curConfig as $for => $to) { + $this->preferences[ltrim($for, '\\')] = ltrim($to, '\\'); + } + break; + + default: + $key = ltrim($key, '\\'); + if (isset($curConfig['type'])) { + $this->virtualTypes[$key] = ltrim($curConfig['type'], '\\'); + } + if (isset($curConfig['arguments'])) { + if (!empty($this->mergedArguments)) { + $this->mergedArguments = []; + } + if (isset($this->arguments[$key])) { + $this->arguments[$key] = array_replace($this->arguments[$key], $curConfig['arguments']); + } else { + $this->arguments[$key] = $curConfig['arguments']; + } + } + if (isset($curConfig['shared'])) { + if (!$curConfig['shared']) { + $this->nonShared[$key] = 1; + } else { + unset($this->nonShared[$key]); + } + } + break; + } + } + } + + /** + * Extend configuration + * + * @param array $configuration + * @return void + */ + public function extend(array $configuration) + { + $this->mergeConfiguration($configuration); + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/ArgumentParser.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/ArgumentParser.php new file mode 100644 index 000000000..33089e7a0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/ArgumentParser.php @@ -0,0 +1,49 @@ +getConverter()->convert($argumentNode, 'argument'); + } + + /** + * Retrieve instance of XML converter, suitable for DI argument nodes + * + * @return FlatConverter + */ + protected function getConverter() + { + if (!$this->converter) { + $arrayNodeConfig = new ArrayNodeConfig(new NodePathMatcher(), ['argument(/item)+' => 'name']); + $this->converter = new FlatConverter($arrayNodeConfig); + } + return $this->converter; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/Dom.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/Dom.php new file mode 100644 index 000000000..bf11e0003 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/Dom.php @@ -0,0 +1,123 @@ +argumentInterpreter = $argumentInterpreter; + $this->booleanUtils = $booleanUtils ?: new BooleanUtils(); + $this->argumentParser = $argumentParser ?: new ArgumentParser(); + } + + /** + * Convert configuration in DOM format to assoc array that can be used by object manager + * + * @param \DOMDocument $config + * @return array + * @throws \Exception + * @todo this method has high cyclomatic complexity in order to avoid performance issues + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function convert($config) + { + $output = []; + /** @var \DOMNode $node */ + foreach ($config->documentElement->childNodes as $node) { + if ($node->nodeType != XML_ELEMENT_NODE) { + continue; + } + switch ($node->nodeName) { + case 'preference': + $output['preferences'][$node->attributes->getNamedItem( + 'for' + )->nodeValue] = $node->attributes->getNamedItem( + 'type' + )->nodeValue; + break; + case 'type': + case 'virtualType': + $typeData = []; + $typeNodeAttributes = $node->attributes; + $typeNodeShared = $typeNodeAttributes->getNamedItem('shared'); + if ($typeNodeShared) { + $typeData['shared'] = $this->booleanUtils->toBoolean($typeNodeShared->nodeValue); + } + if ($node->nodeName == 'virtualType') { + $attributeType = $typeNodeAttributes->getNamedItem('type'); + // attribute type is required for virtual type only in merged configuration + if ($attributeType) { + $typeData['type'] = $attributeType->nodeValue; + } + } + $typeArguments = []; + /** @var \DOMNode $typeChildNode */ + foreach ($node->childNodes as $typeChildNode) { + if ($typeChildNode->nodeType != XML_ELEMENT_NODE) { + continue; + } + switch ($typeChildNode->nodeName) { + case 'arguments': + /** @var \DOMNode $argumentNode */ + foreach ($typeChildNode->childNodes as $argumentNode) { + if ($argumentNode->nodeType != XML_ELEMENT_NODE) { + continue; + } + $argumentName = $argumentNode->attributes->getNamedItem('name')->nodeValue; + $argumentData = $this->argumentParser->parse($argumentNode); + $typeArguments[$argumentName] = $this->argumentInterpreter->evaluate( + $argumentData + ); + } + break; + default: + throw new \Exception( + "Invalid application config. Unknown node: {$typeChildNode->nodeName}." + ); + } + } + + $typeData['arguments'] = $typeArguments; + $output[$typeNodeAttributes->getNamedItem('name')->nodeValue] = $typeData; + break; + default: + throw new \Exception("Invalid application config. Unknown node: {$node->nodeName}."); + } + } + + return $output; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/Dom.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/Dom.php new file mode 100644 index 000000000..876e949cb --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/Dom.php @@ -0,0 +1,70 @@ + 'for', + '/config/(type|virtualType)' => 'name', + '/config/(type|virtualType)/arguments/argument' => 'name', + '/config/(type|virtualType)/arguments/argument(/item)+' => 'name' + ], + $domDocumentClass = 'Magento\FunctionalTestingFramework\Config\Dom', + $defaultScope = 'etc' + ) { + parent::__construct( + $fileResolver, + $converter, + $schemaLocator, + $validationState, + $fileName, + $idAttributes, + $domDocumentClass, + $defaultScope + ); + } + + /** + * Create and return a config merger instance that takes into account types of arguments + * + * @param string $mergerClass + * @param string $initialContents + * @return \Magento\FunctionalTestingFramework\Config\Dom + */ + protected function _createConfigMerger($mergerClass, $initialContents) + { + return new $mergerClass($initialContents, $this->_idAttributes, self::TYPE_ATTRIBUTE, $this->_perFileSchema); + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/DomFactory.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/DomFactory.php new file mode 100644 index 000000000..78e977625 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/DomFactory.php @@ -0,0 +1,53 @@ +objectManager = $objectManager; + $this->instanceName = $instanceName; + } + + /** + * Create class instance with specified parameters + * + * @param array $data + * @return \Magento\FunctionalTestingFramework\ObjectManager\Config\Reader\Dom + */ + public function create(array $data = []) + { + return $this->objectManager->create($this->instanceName, $data); + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/SchemaLocator.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/SchemaLocator.php new file mode 100644 index 000000000..fd4f92b17 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/SchemaLocator.php @@ -0,0 +1,37 @@ +createArgumentInterpreter() + ), + new \Magento\FunctionalTestingFramework\ObjectManager\Config\SchemaLocator(), + new \Magento\FunctionalTestingFramework\Config\ValidationState($this->appMode) + ); + + return $reader->read(); + } + + + /** + * Return newly created instance on an argument interpreter, suitable for processing DI arguments + * + * @return \Magento\FunctionalTestingFramework\Data\Argument\InterpreterInterface + */ + protected function createArgumentInterpreter() + { + $booleanUtils = new \Magento\FunctionalTestingFramework\Stdlib\BooleanUtils(); + $constInterpreter = new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Constant(); + $result = new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Composite( + [ + 'boolean' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Boolean($booleanUtils), + 'string' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\StringUtils($booleanUtils), + 'number' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Number(), + 'null' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\NullType(), + 'object' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\DataObject($booleanUtils), + 'const' => $constInterpreter, + 'init_parameter' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Argument($constInterpreter) + ], + \Magento\FunctionalTestingFramework\ObjectManager\Config\Reader\Dom::TYPE_ATTRIBUTE + ); + // Add interpreters that reference the composite + $result->addInterpreter('array', new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\ArrayType($result)); + return $result; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Definition/Runtime.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Definition/Runtime.php new file mode 100644 index 000000000..f77e7d110 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Definition/Runtime.php @@ -0,0 +1,70 @@ +reader = $reader ? : new \Magento\FunctionalTestingFramework\Code\Reader\ClassReader(); + } + + /** + * Get list of method parameters + * + * Retrieve an ordered list of constructor parameters. + * Each value is an array with following entries: + * + * array( + * 0, // string: Parameter name + * 1, // string|null: Parameter type + * 2, // bool: whether this param is required + * 3, // mixed: default value + * ); + * + * @param string $className + * @return array|null + */ + public function getParameters($className) + { + if (!array_key_exists($className, $this->definitions)) { + $this->definitions[$className] = $this->reader->getConstructor($className); + } + return $this->definitions[$className]; + } + + /** + * Retrieve list of all classes covered with definitions + * + * @return array + */ + public function getClasses() + { + return []; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/DefinitionInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManager/DefinitionInterface.php new file mode 100644 index 000000000..124230fa3 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/DefinitionInterface.php @@ -0,0 +1,38 @@ +classReader = new ClassReader(); + } + + // @codingStandardsIgnoreStart + /** + * Invoke class method and prepared arguments + * + * @param mixed $object + * @param string $method + * @param array $args + * @return mixed + */ + public function invoke($object, $method, array $args = []) + { + $args = $this->prepareArguments($object, $method, $args); + + $type = get_class($object); + $class = new \ReflectionClass($type); + $method = $class->getMethod($method); + + return $method->invokeArgs($object, $args); + } + // @codingStandardsIgnoreEnd + + /** + * Get list of parameters for class method + * + * @param string $type + * @param string $method + * @return array|null + */ + public function getParameters($type, $method) + { + return $this->classReader->getParameters($type, $method); + } + + /** + * Resolve and prepare arguments for class method + * + * @param object $object + * @param string $method + * @param array $arguments + * @return array + */ + public function prepareArguments($object, $method, array $arguments = []) + { + $type = get_class($object); + $parameters = $this->classReader->getParameters($type, $method); + if ($parameters == null) { + return []; + } + + return $this->resolveArguments($type, $parameters, $arguments); + } + + /** + * Resolve constructor arguments + * + * @param string $requestedType + * @param array $parameters + * @param array $arguments + * @return array + * @throws \UnexpectedValueException + * @throws \BadMethodCallException + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function resolveArguments($requestedType, array $parameters, array $arguments = []) + { + $resolvedArguments = []; + $arguments = count($arguments) + ? array_replace($this->config->getArguments($requestedType), $arguments) + : $this->config->getArguments($requestedType); + foreach ($parameters as $parameter) { + list($paramName, $paramType, $paramRequired, $paramDefault) = $parameter; + $argument = null; + if (array_key_exists($paramName, $arguments)) { + $argument = $arguments[$paramName]; + } elseif (array_key_exists('options', $arguments) && array_key_exists($paramName, $arguments['options'])) { + // The parameter name doesn't exist in the arguments, but it is contained in the 'options' argument. + $argument = $arguments['options'][$paramName]; + } else { + if ($paramRequired) { + if ($paramType) { + $argument = ['instance' => $paramType]; + } else { + $this->creationStack = []; + throw new \BadMethodCallException( + 'Missing required argument $' . $paramName . ' of ' . $requestedType . '.' + ); + } + } else { + $argument = $paramDefault; + } + } + if ($paramType && !is_object($argument) && $argument !== $paramDefault) { + if (!is_array($argument)) { + throw new \UnexpectedValueException( + 'Invalid parameter configuration provided for $' . $paramName . ' argument of ' . $requestedType + ); + } + if (isset($argument['instance']) && !empty($argument['instance'])) { + $argumentType = $argument['instance']; + unset($argument['instance']); + if (array_key_exists('shared', $argument)) { + $isShared = $argument['shared']; + unset($argument['shared']); + } else { + $isShared = $this->config->isShared($argumentType); + } + } else { + $argumentType = $paramType; + $isShared = $this->config->isShared($argumentType); + } + + $_arguments = !empty($argument) ? $argument : []; + + $argument = $isShared + ? $this->objectManager->get($argumentType) + : $this->objectManager->create($argumentType, $_arguments); + } else { + if (is_array($argument)) { + if (isset($argument['argument'])) { + $argKey = $argument['argument']; + $argument = isset($this->globalArguments[$argKey]) + ? $this->globalArguments[$argKey] + : $paramDefault; + } else { + $this->parseArray($argument); + } + } + } + $resolvedArguments[$paramName] = $argument; + } + return $resolvedArguments; + } + + /** + * Parse array argument + * + * @param array &$array + * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function parseArray(&$array) + { + foreach ($array as $key => $item) { + if (!is_array($item)) { + continue; + } + if (isset($item['instance'])) { + $itemType = $item['instance']; + $isShared = isset($item['shared']) ? $item['shared'] : $this->config->isShared($itemType); + + unset($item['instance']); + if (array_key_exists('shared', $item)) { + unset($item['shared']); + } + + $_arguments = !empty($item) ? $item : []; + + $array[$key] = $isShared + ? $this->objectManager->get($itemType) + : $this->objectManager->create($itemType, $_arguments); + } elseif (isset($item['argument'])) { + $array[$key] = isset($this->globalArguments[$item['argument']]) + ? $this->globalArguments[$item['argument']] + : null; + } else { + $this->parseArray($item); + } + } + } + + /** + * Create instance with call time arguments + * + * @param string $requestedType + * @param array $arguments + * @return object + * @throws \Exception + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function create($requestedType, array $arguments = []) + { + $instanceType = $this->config->getInstanceType($requestedType); + $parameters = $this->definitions->getParameters($instanceType); + if ($parameters == null) { + return new $instanceType(); + } + if (isset($this->creationStack[$requestedType])) { + $lastFound = end($this->creationStack); + $this->creationStack = []; + throw new \LogicException("Circular dependency: {$requestedType} depends on {$lastFound} and vice versa."); + } + $this->creationStack[$requestedType] = $requestedType; + try { + $args = $this->resolveArguments($requestedType, $parameters, $arguments); + unset($this->creationStack[$requestedType]); + } catch (\Exception $e) { + unset($this->creationStack[$requestedType]); + throw $e; + } + + $reflection = new \ReflectionClass($instanceType); + + return $reflection->newInstanceArgs($args); + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php new file mode 100644 index 000000000..5e452e55f --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php @@ -0,0 +1,213 @@ +config = $config; + $this->objectManager = $objectManager; + $this->definitions = $definitions ?: new \Magento\FunctionalTestingFramework\ObjectManager\Definition\Runtime(); + $this->globalArguments = $globalArguments; + } + + /** + * Set object manager + * + * @param \Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager + * @return void + */ + public function setObjectManager(\Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * Resolve constructor arguments + * + * @param string $requestedType + * @param array $parameters + * @param array $arguments + * @return array + * @throws \UnexpectedValueException + * @throws \BadMethodCallException + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function resolveArguments($requestedType, array $parameters, array $arguments = []) + { + $resolvedArguments = []; + $arguments = count($arguments) + ? array_replace($this->config->getArguments($requestedType), $arguments) + : $this->config->getArguments($requestedType); + foreach ($parameters as $parameter) { + list($paramName, $paramType, $paramRequired, $paramDefault) = $parameter; + $argument = null; + if (!empty($arguments) && (isset($arguments[$paramName]) || array_key_exists($paramName, $arguments))) { + $argument = $arguments[$paramName]; + } elseif ($paramRequired) { + if ($paramType) { + $argument = ['instance' => $paramType]; + } else { + $this->creationStack = []; + throw new \BadMethodCallException( + 'Missing required argument $' . $paramName . ' of ' . $requestedType . '.' + ); + } + } else { + $argument = $paramDefault; + } + if ($paramType && $argument !== $paramDefault && !is_object($argument)) { + if (!isset($argument['instance']) || !is_array($argument)) { + throw new \UnexpectedValueException( + 'Invalid parameter configuration provided for $' . $paramName . ' argument of ' . $requestedType + ); + } + $argumentType = $argument['instance']; + $isShared = (isset($argument['shared']) ? $argument['shared'] : $this->config->isShared($argumentType)); + $argument = $isShared + ? $this->objectManager->get($argumentType) + : $this->objectManager->create($argumentType); + } elseif (is_array($argument)) { + if (isset($argument['argument'])) { + $argument = isset($this->globalArguments[$argument['argument']]) + ? $this->globalArguments[$argument['argument']] + : $paramDefault; + } elseif (!empty($argument)) { + $this->parseArray($argument); + } + } + $resolvedArguments[] = $argument; + } + return $resolvedArguments; + } + + /** + * Parse array argument + * + * @param array &$array + * @return void + */ + protected function parseArray(&$array) + { + foreach ($array as $key => $item) { + if (is_array($item)) { + if (isset($item['instance'])) { + $itemType = $item['instance']; + $isShared = (isset($item['shared'])) ? $item['shared'] : $this->config->isShared($itemType); + $array[$key] = $isShared + ? $this->objectManager->get($itemType) + : $this->objectManager->create($itemType); + } elseif (isset($item['argument'])) { + $array[$key] = isset($this->globalArguments[$item['argument']]) + ? $this->globalArguments[$item['argument']] + : null; + } else { + $this->parseArray($array[$key]); + } + } + } + } + + /** + * Create instance with call time arguments + * + * @param string $requestedType + * @param array $arguments + * @return object + * @throws \Exception + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPCPD) + */ + public function create($requestedType, array $arguments = []) + { + $type = $this->config->getInstanceType($requestedType); + $parameters = $this->definitions->getParameters($type); + if ($parameters == null) { + return new $type(); + } + if (isset($this->creationStack[$requestedType])) { + $lastFound = end($this->creationStack); + $this->creationStack = []; + throw new \LogicException("Circular dependency: {$requestedType} depends on {$lastFound} and vice versa."); + } + $this->creationStack[$requestedType] = $requestedType; + try { + $args = $this->resolveArguments($requestedType, $parameters, $arguments); + unset($this->creationStack[$requestedType]); + } catch (\Exception $e) { + unset($this->creationStack[$requestedType]); + throw $e; + } + + $reflection = new \ReflectionClass($type); + + return $reflection->newInstanceArgs($args); + } + + /** + * Set global arguments + * + * @param array $arguments + * @return void + */ + public function setArguments($arguments) + { + $this->globalArguments = $arguments; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/FactoryInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManager/FactoryInterface.php new file mode 100644 index 000000000..eca6f5bb1 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/FactoryInterface.php @@ -0,0 +1,23 @@ +config = $config; + $this->factory = $factory; + $this->sharedInstances = $sharedInstances; + $this->sharedInstances[\Magento\FunctionalTestingFramework\ObjectManagerInterface::class] = $this; + } + + /** + * Create new object instance + * + * @param string $type + * @param array $arguments + * @return object + */ + public function create($type, array $arguments = []) + { + return $this->factory->create($this->config->getPreference($type), $arguments); + } + + /** + * Retrieve cached object instance + * + * @param string $type + * @return object + */ + public function get($type) + { + $type = $this->config->getPreference($type); + if (!isset($this->sharedInstances[$type])) { + $this->sharedInstances[$type] = $this->factory->create($type); + } + return $this->sharedInstances[$type]; + } + + /** + * Configure di instance + * + * @param array $configuration + * @return void + */ + public function configure(array $configuration) + { + $this->config->extend($configuration); + } + + /** + * Avoid to serialize Closure properties + * + * @return array + */ + public function __sleep() + { + return []; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php new file mode 100644 index 000000000..4da8b6de0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php @@ -0,0 +1,60 @@ +classReader = $classReader ? : new \Magento\FunctionalTestingFramework\Code\Reader\ClassReader(); + } + + /** + * Check whether requested type is available for read + * + * @param string $type + * @return bool + */ + public function has($type) + { + return class_exists($type) || interface_exists($type); + } + + /** + * Retrieve list of parents + * + * @param string $type + * @return array + */ + public function getParents($type) + { + if (!class_exists($type)) { + return $this->default; + } + return $this->classReader->getParents($type) ? : $this->default; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/RelationsInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManager/RelationsInterface.php new file mode 100644 index 000000000..51245710f --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/RelationsInterface.php @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Param name should be unique in scope of type + + + + + + + + + + + Param name should be unique in scope of virtual type + + + + + + + + + + + + + Preference for each class should be unique in scope of file + + + + + + + + + Type name should be unique in scope of file + + + + + + + + + Virtual type name should be unique in scope of file + + + + + + + + + + + Preference help Object Manager to choose class for corresponding interface + + + + + + + + + + With 'type' tag you can point parameters for certain class + + + + + + + + + + + + + + + + + + + + With 'virtualType' tag you can point parameters for autogenerated class + + + + + + + diff --git a/src/Magento/FunctionalTestingFramework/ObjectManagerFactory.php b/src/Magento/FunctionalTestingFramework/ObjectManagerFactory.php new file mode 100644 index 000000000..c858a4035 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManagerFactory.php @@ -0,0 +1,120 @@ +configClassName(); + + $factory = new Factory($diConfig); + $argInterpreter = $this->createArgumentInterpreter(new BooleanUtils()); + $argumentMapper = new \Magento\FunctionalTestingFramework\ObjectManager\Config\Mapper\Dom($argInterpreter); + + + $sharedInstances['Magento\FunctionalTestingFramework\Data\Argument\InterpreterInterface'] = $argInterpreter; + $sharedInstances['Magento\FunctionalTestingFramework\ObjectManager\Config\Mapper\Dom'] = $argumentMapper; + + /** @var \Magento\FunctionalTestingFramework\ObjectManager $objectManager */ + $objectManager = new $this->locatorClassName($factory, $diConfig, $sharedInstances); + + $factory->setObjectManager($objectManager); + ObjectManager::setInstance($objectManager); + + self::configure($objectManager); + + return $objectManager; + } + + /** + * Return newly created instance on an argument interpreter, suitable for processing DI arguments. + * + * @param \Magento\FunctionalTestingFramework\Stdlib\BooleanUtils $booleanUtils + * @return \Magento\FunctionalTestingFramework\Data\Argument\InterpreterInterface + */ + protected function createArgumentInterpreter( + \Magento\FunctionalTestingFramework\Stdlib\BooleanUtils $booleanUtils + ) { + $constInterpreter = new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Constant(); + $result = new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Composite( + [ + 'boolean' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Boolean($booleanUtils), + 'string' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\StringUtils($booleanUtils), + 'number' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Number(), + 'null' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\NullType(), + 'const' => $constInterpreter, + 'object' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\DataObject($booleanUtils), + 'init_parameter' => new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Argument($constInterpreter), + ], + \Magento\FunctionalTestingFramework\ObjectManager\Config\Reader\Dom::TYPE_ATTRIBUTE + ); + // Add interpreters that reference the composite + $result->addInterpreter('array', new \Magento\FunctionalTestingFramework\Data\Argument\Interpreter\ArrayType($result)); + return $result; + } + + /** + * Get Object Manager instance. + * + * @return ObjectManager + */ + public static function getObjectManager() + { + if (!$objectManager = ObjectManager::getInstance()) { + $objectManagerFactory = new self(); + $objectManager = $objectManagerFactory->create(); + } + + return $objectManager; + } + + /** + * Configure Object Manager. + * This method is static to have the ability to configure multiple instances of Object manager when needed. + * + * @param \Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager + * @return void + */ + public static function configure(\Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager) + { + $objectManager->configure( + $objectManager->get(\Magento\FunctionalTestingFramework\ObjectManager\ConfigLoader\Primary::class)->load() + ); + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManagerInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManagerInterface.php new file mode 100644 index 000000000..4e7c9da1d --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/ObjectManagerInterface.php @@ -0,0 +1,38 @@ +initPageObjects(); + } + + return self::$PAGE_DATA_PROCESSOR; + } + + /** + * PageObjectHandler constructor. + */ + private function __construct() + { + //private constructor + } + + /** + * Takes a page name and returns an array parsed from xml. + * + * @param string $pageName + * @return PageObject | null + */ + public function getObject($pageName) + { + if (array_key_exists($pageName, $this->pages)) { + return $this->getAllObjects()[$pageName]; + } + + return null; + } + + /** + * Return an array containing all pages parsed from xml. + * + * @return array + */ + public function getAllObjects() + { + return $this->pages; + } + + /** + * Executes parser code to read in page xml data. + * + * @return void + */ + private function initPageObjects() + { + $objectManager = ObjectManagerFactory::getObjectManager(); + /** @var $parser \Magento\FunctionalTestingFramework\XmlParser\PageParser */ + $parser = $objectManager->get(PageParser::class); + foreach ($parser->getData(self::TYPE) as $pageName => $pageData) { + $urlPath = $pageData[PageObjectHandler::URL_PATH_ATTR]; + $module = $pageData[PageObjectHandler::MODULE_ATTR]; + $sections = array_keys($pageData[PageObjectHandler::SUB_TYPE]); + $parameterized = $pageData[PageObjectHandler::PARAMETERIZED] ?? false; + + $this->pages[$pageName] = new PageObject($pageName, $urlPath, $module, $sections, $parameterized); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php new file mode 100644 index 000000000..e4da7f787 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php @@ -0,0 +1,121 @@ +initSectionObjects(); + } + + return self::$SECTION_DATA_PROCESSOR; + } + + /** + * SectionObjectHandler constructor. + * @constructor + */ + private function __construct() + { + // private constructor + } + + /** + * Returns the corresponding section array parsed from xml. + * + * @param string $sectionName + * @return SectionObject | null + */ + public function getObject($sectionName) + { + if (array_key_exists($sectionName, $this->getAllObjects())) { + return $this->getAllObjects()[$sectionName]; + } + + return null; + } + + /** + * Returns all section arrays parsed from section xml. + * + * @return array + */ + public function getAllObjects() + { + return $this->sectionData; + } + + /** + * Parse section objects if it's not previously done. + * + * @return void + */ + private function initSectionObjects() + { + $objectManager = ObjectManagerFactory::getObjectManager(); + /** @var $parser \Magento\FunctionalTestingFramework\XmlParser\SectionParser */ + $parser = $objectManager->get(SectionParser::class); + foreach ($parser->getData(self::TYPE) as $sectionName => $sectionData) { + // create elements + $elements = []; + foreach ($sectionData[SectionObjectHandler::SUB_TYPE] as $elementName => $elementData) { + $elementType = $elementData[SectionObjectHandler::ELEMENT_TYPE_ATTR]; + $elementLocator = $elementData[SectionObjectHandler::ELEMENT_LOCATOR_ATTR]; + $elementTimeout = $elementData[SectionObjectHandler::ELEMENT_TIMEOUT_ATTR] ?? null; + $elementParameterized = $elementData[SectionObjectHandler::ELEMENT_PARAMETERIZED] ?? false; + + $elements[$elementName] = new ElementObject( + $elementName, + $elementType, + $elementLocator, + $elementTimeout, + $elementParameterized + ); + } + + $this->sectionData[$sectionName] = new SectionObject($sectionName, $elements); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php new file mode 100644 index 000000000..4d867ad08 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php @@ -0,0 +1,121 @@ +name = $name; + $this->type = $type; + $this->locator = $locator; + $this->timeout = $timeout; + $this->parameterized = $parameterized; + } + + /** + * Getter for the name of the element + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Getter for the name of the element type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Getter for the locator of an element + * + * @return string + */ + public function getLocator() + { + return $this->locator; + } + + /** + * Returns an integer representing an element's timeout + * + * @return int|null + */ + public function getTimeout() + { + if ($this->timeout == ElementObject::DEFAULT_TIMEOUT_SYMBOL) { + return null; + } + + return (int)$this->timeout; + } + + /** + * Determines if the element's selector is parameterized. Based on $parameterized property. + * + * @return bool + */ + + public function isParameterized() + { + return $this->parameterized; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php new file mode 100644 index 000000000..7a08b2714 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php @@ -0,0 +1,145 @@ +name = $name; + $this->url = $urlPath; + $this->module = $module; + $this->sectionNames = $sections; + $this->parameterized = $parameterized; + } + + /** + * Getter for Page Name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Getter for Page URL + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Getter for Page Module + * + * @return string + */ + public function getModule() + { + return $this->module; + } + + /** + * Getter for Section Names + * + * @return array + */ + public function getSectionNames() + { + return $this->sectionNames; + } + + /** + * Checks the section names in the page for existence of the section name passed into the method. + * + * @param string $sectionName + * @return boolean + */ + public function hasSection($sectionName) + { + return in_array($sectionName, $this->sectionNames); + } + + /** + * Given a section name referenced by the page, returns the section object + * + * @param string $sectionName + * @return SectionObject | null + */ + public function getSection($sectionName) + { + if ($this->hasSection($sectionName)) { + return SectionObjectHandler::getInstance()->getObject($sectionName); + } + + return null; + } + + /** + * Determines if the page's url is parameterized. Based on $parameterized property. + * + * @return bool + */ + + public function isParameterized() + { + return $this->parameterized; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php new file mode 100644 index 000000000..f9376f9d4 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php @@ -0,0 +1,69 @@ +name = $name; + $this->elements = $elements; + } + + /** + * Getter for the name of the section + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Getter for an array containing all of a section's elements. + * + * @return array + */ + public function getElements() + { + return $this->elements; + } + + /** + * Given the name of an element, returns the element object + * + * @param string $elementName + * @return ElementObject + */ + public function getElement($elementName) + { + return $this->elements[$elementName]; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd b/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd new file mode 100644 index 000000000..aca2a5ee9 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd @@ -0,0 +1,92 @@ + + + + + The definition of a page object. + + + + + + The root element for configuration data. + + + + + + + + Contains sequence of ui sections in a page. + + + + + + + + + + + + + + Contains sequence of ui elements. + + + + + + + + Unique page name identifier. + + + + + + + Url path (excluding the base url) for the page. Use "%s" for placeholders for variables. + + + + + + + The name of the module to which the page belongs. For example: "Magento_Catalog". + + + + + + + + + + + + + + Unique section name identifier. + + + + + + + + + + + + + + + + + + Set to true to remove this element during parsing. + + + + + diff --git a/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd b/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd new file mode 100644 index 000000000..d69706881 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd @@ -0,0 +1,127 @@ + + + + + + + The root element for configuration data. + + + + + + + + Contains sequence of ui elements in a section of a page. + + + + + + + + + + + + + + Contains information of an ui element. + + + + + + + + Unique section name identifier. + + + + + + + + + + + + + Element name. + + + + + + + The type of the element, e.g. select, radio, etc. + + + + + + + Locator of the element. Use %s for placeholders for variables. + + + + + + + Optional variable names separated by "," which are used to substitute %s in locator attribute. + + + + + + + Optional timeout value in second to wait for the operation on the element. use "-" for default value. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Set to true to remove this element during parsing. + + + + + diff --git a/src/Magento/FunctionalTestingFramework/Stdlib/BooleanUtils.php b/src/Magento/FunctionalTestingFramework/Stdlib/BooleanUtils.php new file mode 100644 index 000000000..70f933ec6 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Stdlib/BooleanUtils.php @@ -0,0 +1,65 @@ +trueValues = $trueValues; + $this->falseValues = $falseValues; + } + + /** + * Retrieve boolean value for an expression + * + * @param mixed $value Boolean expression + * @return bool + * @throws \InvalidArgumentException + */ + public function toBoolean($value) + { + /** + * Built-in function filter_var() is not used, because such values as on/off are irrelevant in some contexts + * @link http://www.php.net/manual/en/filter.filters.validate.php + */ + if (in_array($value, $this->trueValues, true)) { + return true; + } + if (in_array($value, $this->falseValues, true)) { + return false; + } + $allowedValues = array_merge($this->trueValues, $this->falseValues); + throw new \InvalidArgumentException( + 'Boolean value is expected, supported values: ' . var_export($allowedValues, true) + ); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Step/Backend/AdminStep.php b/src/Magento/FunctionalTestingFramework/Step/Backend/AdminStep.php new file mode 100644 index 000000000..8284de253 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Step/Backend/AdminStep.php @@ -0,0 +1,2307 @@ +openNewTab(); + $I->amOnPage($url); + $I->waitForPageLoad(); + $I->seeInCurrentUrl($url); + } + + public function closeNewTab() + { + $I = $this; + $I->closeTab(); + } + + // Key Admin Pages + public function goToRandomAdminPage() + { + $I = $this; + + $admin_url_list = array( + "/admin/admin/dashboard/", + "/admin/sales/order/", + "/admin/sales/invoice/", + "/admin/sales/shipment/", + "/admin/sales/creditmemo/", + "/admin/paypal/billing_agreement/", + "/admin/sales/transactions/", + "/admin/catalog/product/", + "/admin/catalog/category/", + "/admin/customer/index/", + "/admin/customer/online/", + "/admin/catalog_rule/promo_catalog/", + "/admin/sales_rule/promo_quote/", + "/admin/admin/email_template/", + "/admin/newsletter/template/", + "/admin/newsletter/queue/", + "/admin/newsletter/subscriber/", + "/admin/admin/url_rewrite/index/", + "/admin/search/term/index/", + "/admin/search/synonyms/index/", + "/admin/admin/sitemap/", + "/admin/review/product/index/", + "/admin/cms/page/", + "/admin/cms/block/", + "/admin/admin/widget_instance/", + "/admin/theme/design_config/", + "/admin/admin/system_design_theme/", + "/admin/admin/system_design/", + "/admin/reports/report_shopcart/product/", + "/admin/search/term/report/", + "/admin/reports/report_shopcart/abandoned/", + "/admin/newsletter/problem/", + "/admin/reports/report_review/customer/", + "/admin/reports/report_review/product/", + "/admin/reports/report_sales/sales/", + "/admin/reports/report_sales/tax/", + "/admin/reports/report_sales/invoiced/", + "/admin/reports/report_sales/shipping/", + "/admin/reports/report_sales/refunded/", + "/admin/reports/report_sales/coupons/", + "/admin/paypal/paypal_reports/", + "/admin/braintree/report/", + "/admin/reports/report_customer/totals/", + "/admin/reports/report_customer/orders/", + "/admin/reports/report_customer/accounts/", + "/admin/reports/report_product/viewed/", + "/admin/reports/report_sales/bestsellers/", + "/admin/reports/report_product/lowstock/", + "/admin/reports/report_product/sold/", + "/admin/reports/report_product/downloads/", + "/admin/reports/report_statistics/", + "/admin/admin/system_store/", + "/admin/admin/system_config/", + "/admin/checkout/agreement/", + "/admin/sales/order_status/", + "/admin/tax/rule/", + "/admin/tax/rate/", + "/admin/admin/system_currency/", + "/admin/admin/system_currencysymbol/", + "/admin/catalog/product_attribute/", + "/admin/catalog/product_set/", + "/admin/review/rating/", + "/admin/customer/group/", + "/admin/admin/import/", + "/admin/admin/export/", + "/admin/tax/rate/importExport/", + "/admin/admin/history/", + "/admin/admin/integration/", + "/admin/admin/cache/", + "/admin/backup/index/", + "/admin/indexer/indexer/list/", + "/admin/admin/user/", + "/admin/admin/locks/", + "/admin/admin/user_role/", + "/admin/admin/notification/", + "/admin/admin/system_variable/", + "/admin/admin/crypt_key/" + ); + + $random_admin_url = array_rand($admin_url_list, 1); + + $I->amOnPage($admin_url_list[$random_admin_url]); + $I->waitForPageLoad(); + + return $admin_url_list[$random_admin_url]; + } + + public function goToTheAdminLoginPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLoginPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminLogoutPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLogoutPage); + } + + // Sales + public function goToTheAdminOrdersGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrdersGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminOrderForIdPage($orderId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderByIdPage . $orderId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddOrderPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddOrderForCustomerIdPage($customerId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderForCustomerIdPage . $customerId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminInvoicesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminInvoicesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddInvoiceForOrderIdPage($orderId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddInvoiceForOrderIdPage . $orderId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminShipmentsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShipmentsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminShipmentForIdPage($shipmentId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShipmentForIdPage . $shipmentId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminCreditMemosGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreditMemosGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCreditMemoForIdPage($creditMemoId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreditMemoForIdPage . $creditMemoId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminBillingAgreementsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBillingAgreementsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminTransactionsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTransactionsGrid); + $I->waitForPageLoad(); + } + + // Products + public function goToTheAdminCatalogPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminProductForIdPage($productId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductForIdPage . $productId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddSimpleProductPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSimpleProductPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddConfigurableProductPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddConfigurableProductPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddGroupedProductPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddGroupedProductPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddVirtualProductPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddVirtualProductPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddBundledProductPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddBundleProductPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddDownloadableProductPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddDownloadableProductPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCategoriesPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCategoriesPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCategoryForIdPage($categoryId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCategoryForIdPage . $categoryId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddRootCategoryForStoreIdPage($storeId) + { + $I = $this; + $I->amOnPage(('/admin/catalog/category/add/store/' . $storeId . '/parent/1')); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddSubCategoryForStoreIdPage($storeId) + { + $I = $this; + $I->amOnPage(('/admin/catalog/category/add/store/' . $storeId . '/parent/2')); + $I->waitForPageLoad(); + } + + // Customers + public function goToTheAdminAllCustomersGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllCustomersGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCustomersNowOnlineGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomersNowOnlineGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCustomerForIdPage($customerId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerForCustomerIdPage . $customerId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddCustomerPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomerPage); + $I->waitForPageLoad(); + } + + // Marketing + public function goToTheAdminCatalogPriceRuleGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogPriceRuleGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCatalogPriceRuleForIdPage($catalogPriceRuleId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogPriceRuleForIdPage . $catalogPriceRuleId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddCatalogPriceRulePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCatalogPriceRulePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCartPriceRulesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCartPriceRulesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCartPriceRuleForIdPage($cartPriceRuleId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCartPriceRuleForIdPage . $cartPriceRuleId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddCartPriceRulePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCartPriceRulePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminEmailTemplatesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEmailTemplatesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminEmailTemplateForIdPage($emailTemplateId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEmailTemplateForIdPage . $emailTemplateId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddEmailTemplatePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddEmailTemplatePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminNewsletterTemplateGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterTemplateGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminNewsletterTemplateByIdPage($newsletterTemplateId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterTemplateForIdPage . $newsletterTemplateId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddNewsletterTemplatePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewsletterTemplatePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminNewsletterQueueGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterQueueGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminNewsletterSubscribersGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterSubscribersGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminURLRewritesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminURLRewritesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminURLRewriteForId($urlRewriteId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminURLRewriteForIdPage . $urlRewriteId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddURLRewritePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddURLRewritePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminSearchTermsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminSearchTermForIdPage($searchTermId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermForIdPage . $searchTermId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddSearchTermPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSearchTermPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminSearchSynonymsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchSynonymsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminSearchSynonymGroupByIdPage($searchSynonymId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchSynonymGroupForIdPage . $searchSynonymId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddSearchSynonymGroupPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSearchSynonymGroupPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminSiteMapGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSiteMapGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminSiteMapForIdPage($siteMapId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSiteMapForIdPage . $siteMapId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddSiteMapPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSiteMapPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminReviewsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminReviewsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminReviewForIdPage($reviewId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminReviewByIdPage . $reviewId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddReviewPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddReviewPage); + $I->waitForPageLoad(); + } + + // Content + public function goToTheAdminPagesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPagesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminPageForIdPage($pageId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPageForIdPage . $pageId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddPagePage() + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddPagePage)); + $I->waitForPageLoad(); + } + + public function goToTheAdminBlocksGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBlocksGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminBlockForIdPage($blockId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBlockForIdPage . $blockId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddBlockPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddBlockPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminWidgetsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWidgetsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddWidgetPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddWidgetPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminDesignConfigurationGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDesignConfigurationGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminThemesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminThemesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminThemeByIdPage($themeId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminThemeByIdPage . $themeId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminStoreContentScheduleGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreContentScheduleGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminStoreContentScheduleForIdPage($storeContentScheduleId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreContentScheduleForIdPage . $storeContentScheduleId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddStoreDesignChangePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddStoreDesignChangePage); + $I->waitForPageLoad(); + } + + // Reports + public function goToTheAdminProductsInCartGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductsInCartGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminSearchTermsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminAbandonedCartsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAbandonedCartsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminNewsletterProblemsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterProblemsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCustomerReviewsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerReviewsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminProductReviewsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductReviewsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminProductReviewsForProductIdPage($productId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductReviewsForProductIdPage . $productId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminProductReviewIdForProductIdPage($productReviewId, $productId) + { + $I = $this; + $I->amOnPage(('/admin/review/product/edit/id/' . $productReviewId . '/productId/' . $productId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminOrdersReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrdersReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminTaxReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminInvoiceReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminInvoiceReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminShippingReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShippingReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminRefundsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRefundsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCouponsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCouponsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminPayPalSettlementReportsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPayPalSettlementReportsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminBraintreeSettlementReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBraintreeSettlementReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminOrderTotalReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderTotalReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminOrderCountReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderCountReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminNewAccountsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewAccountsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminProductViewsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductViewsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminBestsellersReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBestsellersReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminLowStockReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLowStockReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminOrderedProductsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderedProductsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminDownloadsReportGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDownloadsReportGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminRefreshStatisticsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRefreshStatisticsGrid); + $I->waitForPageLoad(); + } + + // Stores + public function goToTheAdminAllStoresGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllStoresGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCreateStoreViewPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateStoreViewPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCreateStorePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateStorePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCreateWebsitePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateWebsitePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminWebsiteForIdPage($websiteId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWebsiteByIdPage . $websiteId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminStoreViewForIdPage($storeViewId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreViewByIdPage . $storeViewId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminStoreForIdPage($storeId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreByIdPage . $storeId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminConfigurationGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminConfigurationGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminTermsAndConditionsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTermsAndConditionsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminTermsAndConditionForIdPage($termsAndConditionsId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTermsAndConditionByIdPage . $termsAndConditionsId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddNewTermsAndConditionsPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewTermsAndConditionPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminOrderStatusGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderStatusGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddOrderStatusPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderStatusPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminTaxRulesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxRulesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminTaxRuleForIdPage($taxRuleId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxRuleByIdPage . $taxRuleId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddTaxRulePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddTaxRulePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminTaxZonesAndRatesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxZonesAndRatesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminTaxZoneAndRateForIdPage($taxZoneAndRateId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxZoneAndRateByIdPage . $taxZoneAndRateId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddTaxZoneAndRatePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddTaxZoneAndRatePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCurrencyRatesPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCurrencyRatesPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCurrencySymbolsPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCurrencySymbolsPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminProductAttributesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductAttributesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminProductAttributeForIdPage($productAttributeId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductAttributeForIdPage . $productAttributeId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddProductAttributePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddProductAttributePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminAttributeSetGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAttributeSetsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminAttributeSetByIdPage($attributeSetId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAttributeSetByIdPage . $attributeSetId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddAttributeSetPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddAttributeSetPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminRatingGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRatingsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminRatingForIdPage($ratingId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRatingForIdPage . $ratingId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddRatingPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddRatingPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCustomerGroupsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerGroupsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCustomerGroupForIdPage($customerGroupId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerGroupByIdPage . $customerGroupId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddCustomerGroupPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomerGroupPage); + $I->waitForPageLoad(); + } + + // System + public function goToTheAdminImportPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminExportPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminExportPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminImportAndExportTaxRatesPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportAndExportTaxRatesPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminImportHistoryGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportHistoryGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminIntegrationsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIntegrationsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminIntegrationForIdPage($integrationId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIntegrationByIdPage . $integrationId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddIntegrationPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddIntegrationPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminCacheManagementGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCacheManagementGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminBackupsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBackupsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminIndexManagementGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIndexManagementGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminWebSetupWizardPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWebSetupWizardPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminAllUsersGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllUsersGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminUserForIdPage($userId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserByIdPage . $userId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddUserPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewUserPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminLockedUsersGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLockedUsersGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminUserRolesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserRolesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminUserRoleForIdPage($userRoleId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserRoleByIdPage . $userRoleId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddUserRolePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddUserRolePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminNotificationsGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNotificationsGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCustomVariablesGrid() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomVariablesGrid); + $I->waitForPageLoad(); + } + + public function goToTheAdminCustomVariableForId($customVariableId) + { + $I = $this; + $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomVariableByIdPage . $customVariableId)); + $I->waitForPageLoad(); + } + + public function goToTheAdminAddCustomVariablePage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomVariablePage); + $I->waitForPageLoad(); + } + + public function goToTheAdminEncryptionKeyPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEncryptionKeyPage); + $I->waitForPageLoad(); + } + + public function goToTheAdminFindPartnersAndExtensionsPage() + { + $I = $this; + $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminFindPartnersAndExtensions); + $I->waitForPageLoad(); + } + + // Key Admin Pages + public function shouldBeOnTheAdminLoginPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLoginPage); + } + + public function shouldBeOnTheAdminDashboardPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDashboardPage); + $I->see('Dashboard', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminForgotYourPasswordPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminForgotYourPasswordPage); + } + + // Sales + public function shouldBeOnTheAdminOrdersGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrdersGrid); + $I->see('Orders', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminOrderForIdPage($orderId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderByIdPage . $orderId)); + $I->see($orderId, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddOrderPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderPage); + $I->see('Create New Order', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddOrderForCustomerIdPage($customerId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderForCustomerIdPage . $customerId)); + $I->see('Create New Order', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminInvoicesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminInvoicesGrid); + $I->see('Invoices', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddInvoiceForOrderIdPage($orderId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddInvoiceForOrderIdPage . $orderId)); + $I->see('New Invoice', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminShipmentsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShipmentsGrid); + $I->see('Shipments', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminShipmentForIdPage($shipmentId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShipmentForIdPage . $shipmentId)); + $I->see('New Shipment'); + } + + public function shouldBeOnTheAdminCreditMemosGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreditMemosGrid); + $I->see('Credit Memos', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCreditMemoForIdPage($creditMemoId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreditMemoForIdPage . $creditMemoId)); + $I->see('View Memo', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminBillingAgreementsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBillingAgreementsGrid); + $I->see('Billing Agreements', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminTransactionsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTransactionsGrid); + $I->see('Transactions', self::$adminPageTitle); + } + + // Products + public function shouldBeOnTheAdminCatalogGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogGrid); + $I->see('Catalog', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminProductForIdPage($productId, $productName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductForIdPage . $productId)); + $I->see($productName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddSimpleProductPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSimpleProductPage); + $I->see('New Product', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddConfigurableProductPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddConfigurableProductPage); + $I->see('New Product', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddGroupedProductPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddGroupedProductPage); + $I->see('New Product', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddVirtualProductPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddVirtualProductPage); + $I->see('New Product', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddBundledProductPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddBundleProductPage); + $I->see('New Product', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddDownloadableProductPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddDownloadableProductPage); + $I->see('New Product', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCategoriesPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCategoriesPage); + $I->see('Default Category', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCategoryForIdPage($categoryId, $categoryName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCategoryForIdPage . $categoryId)); + $I->see($categoryName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddRootCategoryForStoreIdPage($storeId) + { + $I = $this; + $I->seeInCurrentUrl(('/admin/catalog/category/add/store/' . $storeId . '/parent/1')); + $I->see('New Category', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddSubCategoryForStoreIdPage($storeId) + { + $I = $this; + $I->seeInCurrentUrl(('/admin/catalog/category/add/store/' . $storeId . '/parent/2')); + $I->see('New Category', self::$adminPageTitle); + } + + // Customers + public function shouldBeOnTheAdminAllCustomersGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllCustomersGrid); + $I->see('Customers', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCustomersNowOnlineGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomersNowOnlineGrid); + $I->see('Customers Now Online', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCustomerForIdPage($customerId, $customerName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerForCustomerIdPage . $customerId)); + $I->see($customerName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddCustomerPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomerPage); + $I->see('New Customer', self::$adminPageTitle); + } + + // Marketing + public function shouldBeOnTheAdminCatalogPriceRuleGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogPriceRuleGrid); + $I->see('Catalog Price Rule', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCatalogPriceRuleForIdPage($catalogPriceRuleId, $catalogPriceRuleName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogPriceRuleForIdPage . $catalogPriceRuleId)); + $I->see($catalogPriceRuleName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddCatalogPriceRulePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCatalogPriceRulePage); + $I->see('New Catalog Price Rule', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCartPriceRulesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCartPriceRulesGrid); + $I->see('Cart Price Rules', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCartPriceRuleForIdPage($cartPriceRuleId, $cartPriceRuleName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCartPriceRuleForIdPage . $cartPriceRuleId)); + $I->see($cartPriceRuleName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddCartPriceRulePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCartPriceRulePage); + $I->see('New Cart Price Rule', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminEmailTemplatesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEmailTemplatesGrid); + $I->see('Email Templates', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminEmailTemplateForIdPage($emailTemplateId, $templateName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEmailTemplateForIdPage . $emailTemplateId)); + $I->see($templateName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddEmailTemplatePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddEmailTemplatePage); + $I->see('New Template', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminNewsletterTemplateGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterTemplateGrid); + $I->see('Newsletter Templates', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminNewsletterTemplateByIdPage($newsletterTemplateId, $templateName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterTemplateForIdPage . $newsletterTemplateId)); + $I->see($templateName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddNewsletterTemplatePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewsletterTemplatePage); + $I->see('New Template', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminNewsletterQueueGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterQueueGrid); + $I->see('Newsletter Queue', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminNewsletterSubscribersGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterSubscribersGrid); + $I->see('Newsletter Subscribers', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminURLRewritesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminURLRewritesGrid); + $I->see('URL Rewrites', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminURLRewriteForId($urlRewriteId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminURLRewriteForIdPage . $urlRewriteId)); + $I->see('Edit URL Rewrite for a', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddURLRewritePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddURLRewritePage); + $I->see('Add New URL Rewrite', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminSearchTermsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermsGrid); + $I->see('Search Terms', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminSearchTermForIdPage($searchTermId, $searchQuery) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermForIdPage . $searchTermId)); + $I->see($searchQuery, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddSearchTermPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSearchTermPage); + $I->see('New Search', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminSearchSynonymsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchSynonymsGrid); + $I->see('Search Synonyms', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminSearchSynonymGroupByIdPage($searchSynonymId, $synonyms) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchSynonymGroupForIdPage . $searchSynonymId)); + $I->see($synonyms, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddSearchSynonymGroupPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSearchSynonymGroupPage); + $I->see('New Synonym Group', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminSiteMapGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSiteMapGrid); + $I->see('Site Map', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminSiteMapForIdPage($siteMapId, $fileName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSiteMapForIdPage . $siteMapId)); + $I->see($fileName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddSiteMapPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSiteMapPage); + $I->see('New Site Map', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminReviewsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminReviewsGrid); + $I->see('Reviews', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminReviewForIdPage($reviewId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminReviewByIdPage . $reviewId)); + $I->see('Edit Review', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddReviewPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddReviewPage); + $I->see('New Review', self::$adminPageTitle); + } + + // Content + public function shouldBeOnTheAdminPagesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPagesGrid); + $I->see('Pages', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminPageForIdPage($pageId, $pageTitle) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPageForIdPage . $pageId)); + $I->see($pageTitle, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddPagePage() + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddPagePage)); + $I->see('New Page', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminBlocksGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBlocksGrid); + $I->see('Blocks', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminBlockForIdPage($blockId, $blockTitle) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBlockForIdPage . $blockId)); + $I->see($blockTitle, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddBlockPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddBlockPage); + $I->see('New Block', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminWidgetsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWidgetsGrid); + $I->see('Widgets', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddWidgetPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddWidgetPage); + $I->see('Widgets', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminDesignConfigurationGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDesignConfigurationGrid); + $I->see('Design Configuration', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminThemesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminThemesGrid); + $I->see('Themes', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminThemeByIdPage($themeId, $themeTitle) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminThemeByIdPage . $themeId)); + $I->see($themeTitle); + } + + public function shouldBeOnTheAdminStoreContentScheduleGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreContentScheduleGrid); + $I->see('Store Design Schedule', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminStoreContentScheduleForIdPage($storeContentScheduleId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreContentScheduleForIdPage . $storeContentScheduleId)); + $I->see('Edit Store Design Change', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddStoreDesignChangePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddStoreDesignChangePage); + $I->see('New Store Design Change'); + } + + // Reports + public function shouldBeOnTheAdminProductsInCartGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductsInCartGrid); + $I->see('Products in Carts', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminSearchTermsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermsReportGrid); + $I->see('Search Terms Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAbandonedCartsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAbandonedCartsGrid); + $I->see('Abandoned Carts', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminNewsletterProblemsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterProblemsReportGrid); + $I->see('Newsletter Problems Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCustomerReviewsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerReviewsReportGrid); + $I->see('Customer Reviews Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminProductReviewsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductReviewsReportGrid); + $I->see('Product Reviews Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminProductReviewsForProductIdPage($productId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductReviewsForProductIdPage . $productId)); + $I->see('Reviews', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminProductReviewIdForProductIdPage($productReviewId, $productId) + { + $I = $this; + $I->seeInCurrentUrl(('/admin/review/product/edit/id/' . $productReviewId . '/productId/' . $productId)); + $I->see('Edit Review', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminOrdersReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrdersReportGrid); + $I->see('Orders Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminTaxReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxReportGrid); + $I->see('Tax Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminInvoiceReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminInvoiceReportGrid); + $I->see('Invoice Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminShippingReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShippingReportGrid); + $I->see('Shipping Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminRefundsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRefundsReportGrid); + $I->see('Refunds Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCouponsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCouponsReportGrid); + $I->see('Coupons Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminPayPalSettlementReportsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPayPalSettlementReportsGrid); + $I->see('PayPal Settlement Reports', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminBraintreeSettlementReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBraintreeSettlementReportGrid); + $I->see('Braintree Settlement Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminOrderTotalReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderTotalReportGrid); + $I->see('Order Total Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminOrderCountReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderCountReportGrid); + $I->see('Order Count Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminNewAccountsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewAccountsReportGrid); + $I->see('New Accounts Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminProductViewsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductViewsReportGrid); + $I->see('Product Views Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminBestsellersReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBestsellersReportGrid); + $I->see('Bestsellers Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminLowStockReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLowStockReportGrid); + $I->see('Low Stock Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminOrderedProductsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderedProductsReportGrid); + $I->see('Ordered Products Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminDownloadsReportGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDownloadsReportGrid); + $I->see('Downloads Report', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminRefreshStatisticsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRefreshStatisticsGrid); + $I->see('Refresh Statistics', self::$adminPageTitle); + } + + // Stores + public function shouldBeOnTheAdminAllStoresGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllStoresGrid); + $I->see('Stores', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCreateStoreViewPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateStoreViewPage); + $I->see('Stores', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCreateStorePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateStorePage); + $I->see('Stores', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCreateWebsitePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateWebsitePage); + $I->see('Stores'); + } + + public function shouldBeOnTheAdminWebsiteForIdPage($websiteId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWebsiteByIdPage . $websiteId)); + } + + public function shouldBeOnTheAdminStoreViewForIdPage($storeViewId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreViewByIdPage . $storeViewId)); + } + + public function shouldBeOnTheAdminStoreForIdPage($storeId) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreByIdPage . $storeId)); + } + + public function shouldBeOnTheAdminConfigurationGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminConfigurationGrid); + $I->see('Configuration', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminTermsAndConditionsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTermsAndConditionsGrid); + $I->see('Terms and Conditions', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminTermsAndConditionForIdPage($termsAndConditionsId, $conditionName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTermsAndConditionByIdPage . $termsAndConditionsId)); + $I->see($conditionName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddNewTermsAndConditionsPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewTermsAndConditionPage); + $I->see('New Condition', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminOrderStatusGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderStatusGrid); + $I->see('Order Status', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddOrderStatusPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderStatusPage); + $I->see('Create New Order Status', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminTaxRulesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxRulesGrid); + $I->see('Tax Rules', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminTaxRuleForIdPage($taxRuleId, $taxRuleName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxRuleByIdPage . $taxRuleId)); + $I->see($taxRuleName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddTaxRulePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddTaxRulePage); + $I->see('New Tax Rule', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminTaxZonesAndRatesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxZonesAndRatesGrid); + $I->see('Tax Zones and Rates', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminTaxZoneAndRateForIdPage($taxZoneAndRateId, $taxIdentifier) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxZoneAndRateByIdPage . $taxZoneAndRateId)); + $I->see($taxIdentifier, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddTaxZoneAndRatePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddTaxZoneAndRatePage); + $I->see('New Tax Rate', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCurrencyRatesPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCurrencyRatesPage); + $I->see('Currency Rates', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCurrencySymbolsPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCurrencySymbolsPage); + $I->see('Currency Symbols', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminProductAttributesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductAttributesGrid); + $I->see('Product Attributes', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminProductAttributeForIdPage($productAttributeId, $productAttributeDefaultLabel) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductAttributeForIdPage . $productAttributeId)); + $I->see($productAttributeDefaultLabel, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddProductAttributePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddProductAttributePage); + $I->see('New Product Attribute', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAttributeSetsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAttributeSetsGrid); + $I->see('Attribute Sets', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAttributeSetByIdPage($attributeSetId, $attributeSetName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAttributeSetByIdPage . $attributeSetId)); + $I->see($attributeSetName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddAttributeSetPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddAttributeSetPage); + $I->see('New Attribute Set', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminRatingsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRatingsGrid); + $I->see('Ratings', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminRatingForIdPage($ratingId, $ratingDefaultValue) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRatingForIdPage . $ratingId)); + $I->see($ratingDefaultValue, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddRatingPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddRatingPage); + $I->see('New Rating', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCustomerGroupsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerGroupsGrid); + $I->see('Customer Groups', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCustomerGroupForIdPage($customerGroupId, $customerGroupName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerGroupByIdPage . $customerGroupId)); + $I->see($customerGroupName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddCustomerGroupPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomerGroupPage); + $I->see('New Customer Group', self::$adminPageTitle); + } + + // System + public function shouldBeOnTheAdminImportPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportPage); + $I->see('Import', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminExportPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminExportPage); + $I->see('Export', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminImportAndExportTaxRatesPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportAndExportTaxRatesPage); + $I->see('Import and Export Tax Rates', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminImportHistoryGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportHistoryGrid); + $I->see('Import History', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminIntegrationsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIntegrationsGrid); + $I->see('Integrations', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminIntegrationForIdPage($integrationId, $integrationName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIntegrationByIdPage . $integrationId)); + $I->see('Edit', self::$adminPageTitle); + $I->see($integrationName, self::$adminPageTitle); + $I->see('Integration', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddIntegrationPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddIntegrationPage); + $I->see('New Integration', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCacheManagementGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCacheManagementGrid); + $I->see('Cache Management', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminBackupsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBackupsGrid); + $I->see('Backups', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminIndexManagementGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIndexManagementGrid); + $I->see('Index Management', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminWebSetupWizardPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWebSetupWizardPage); + $I->see('Setup Wizard', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAllUsersGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllUsersGrid); + $I->see('Users', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminUserForIdPage($userId, $userFirstAndLastName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserByIdPage . $userId)); + $I->see($userFirstAndLastName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddUserPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewUserPage); + $I->see('New User', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminLockedUsersGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLockedUsersGrid); + $I->see('Locked Users', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminUserRolesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserRolesGrid); + $I->see('Roles', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminUserRoleForIdPage($userRoleId, $userRoleName) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserRoleByIdPage . $userRoleId)); + $I->see($userRoleName, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddUserRolePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddUserRolePage); + $I->see('New Role', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminNotificationsGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNotificationsGrid); + $I->see('Notifications', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCustomVariablesGrid() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomVariablesGrid); + $I->see('Custom Variables', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminCustomVariableForId($customVariableId, $customVariableCode) + { + $I = $this; + $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomVariableByIdPage . $customVariableId)); + $I->see($customVariableCode, self::$adminPageTitle); + } + + public function shouldBeOnTheAdminAddCustomVariablePage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomVariablePage); + $I->see('New Custom Variable', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminEncryptionKeyPage() + { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEncryptionKeyPage); + $I->see('Encryption Key', self::$adminPageTitle); + } + + public function shouldBeOnTheAdminFindPartnersAndExtensionsPage() { + $I = $this; + $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminFindPartnersAndExtensions); + $I->see('Magento Marketplace', self::$adminPageTitle); + } +} diff --git a/src/Magento/FunctionalTestingFramework/System/Code/ClassReader.php b/src/Magento/FunctionalTestingFramework/System/Code/ClassReader.php new file mode 100644 index 000000000..5f44ea104 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/System/Code/ClassReader.php @@ -0,0 +1,51 @@ +getMethod($method); + if ($method) { + $result = []; + /** @var $parameter \ReflectionParameter */ + foreach ($method->getParameters() as $parameter) { + try { + $result[$parameter->getName()] = [ + $parameter->getName(), + ($parameter->getClass() !== null) ? $parameter->getClass()->getName() : null, + !$parameter->isOptional(), + $parameter->isOptional() ? + $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null : + null + ]; + } catch (\ReflectionException $e) { + $message = $e->getMessage(); + throw new \ReflectionException($message, 0, $e); + } + } + } + + return $result; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php b/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php new file mode 100644 index 000000000..bd53b8b5f --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php @@ -0,0 +1,142 @@ +arrayNodeConfig = $arrayNodeConfig; + } + + /** + * Convert config. + * + * @param \DOMDocument $source + * @return array|string + */ + public function convert($source) + { + return $this->convertXml($source); + } + + /** + * Convert dom node tree to array in general case or to string in a case of a text node + * + * Example: + * + * val2 + * + * + * is converted to + * + * array( + * 'node' => array( + * 'attr' => 'wal', + * 'subnode' => 'val2' + * ) + * ) + * + * @param \DOMNode $source + * @param string $basePath + * @return string|array + * @throws \UnexpectedValueException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function convertXml(\DOMNode $source, $basePath = '') + { + $value = []; + /** @var \DOMNode $node */ + foreach ($source->childNodes as $node) { + if ($node->nodeType == XML_ELEMENT_NODE) { + $nodeName = $node->nodeName; + $nodePath = $basePath . '/' . $nodeName; + + $arrayKeyAttribute = $this->arrayNodeConfig->getAssocArrayKeyAttribute($nodePath); + $isNumericArrayNode = $this->arrayNodeConfig->isNumericArray($nodePath); + $isArrayNode = $isNumericArrayNode || $arrayKeyAttribute; + + if (isset($value[$nodeName]) && !$isArrayNode) { + throw new \UnexpectedValueException( + "Node path '{$nodePath}' is not unique, but it has not been marked as array." + ); + } + + $nodeData = $this->convertXml($node, $nodePath); + if ($isArrayNode) { + if ($isNumericArrayNode) { + $value[$nodeName][] = $nodeData; + } elseif (isset($nodeData[$arrayKeyAttribute])) { + $arrayKeyValue = $nodeData[$arrayKeyAttribute]; + $value[$arrayKeyValue] = $nodeData; + } else { + throw new \UnexpectedValueException( + "Array is expected to contain value for key '{$arrayKeyAttribute}'." + ); + } + } else { + $value[$nodeName] = $nodeData; + } + } elseif ($node->nodeType == XML_CDATA_SECTION_NODE + || ($node->nodeType == XML_TEXT_NODE && trim($node->nodeValue) != '') + ) { + $value = $node->nodeValue; + break; + } + } + $result = $this->getNodeAttributes($source); + if (is_array($value)) { + $result = array_merge($result, $value); + if (!$result) { + $result = ''; + } + } else { + if ($result) { + $result['value'] = $value; + } else { + $result = $value; + } + } + return $result; + } + + /** + * Retrieve key-value pairs of node attributes + * + * @param \DOMNode $node + * @return array + */ + protected function getNodeAttributes(\DOMNode $node) + { + $result = ['nodeName' => $node->nodeName]; + $attributes = $node->attributes ?: []; + /** @var \DOMNode $attribute */ + foreach ($attributes as $attribute) { + if ($attribute->nodeType == XML_ATTRIBUTE_NODE) { + $result[$attribute->nodeName] = $attribute->nodeValue; + } + } + return $result; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php new file mode 100644 index 000000000..7ba2b4cbb --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php @@ -0,0 +1,104 @@ +initActionGroups(); + } + + return self::$ACTION_GROUP_OBJECT_HANDLER; + } + + /** + * ActionGroupObjectHandler constructor. + */ + private function __construct() + { + // private constructor + } + + /** + * Function to return a single object by name + * + * @param string $actionGroupName + * @return ActionGroupObject + */ + public function getObject($actionGroupName) + { + return $this->getAllObjects()[$actionGroupName]; + } + + /** + * Function to return all objects for which the handler is responsible + * + * @return array + */ + public function getAllObjects() + { + return $this->actionGroups; + } + + /** + * Method which populates field array with objects from parsed action_group.xml + * + * @return void + */ + private function initActionGroups() + { + $actionGroupParser = ObjectManagerFactory::getObjectManager()->create(ActionGroupDataParser::class); + $parsedActionGroups = $actionGroupParser->readActionGroupData(); + + $actionGroupObjectExtractor = new ActionGroupObjectExtractor(); + + foreach ($parsedActionGroups[ActionGroupObjectHandler::ACTION_GROUP_ROOT] as + $actionGroupName => $actionGroupData) { + if (!is_array($actionGroupData)) { + continue; + } + + $this->actionGroups[$actionGroupName] = + $actionGroupObjectExtractor->extractActionGroup($actionGroupData); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Handlers/CestObjectHandler.php b/src/Magento/FunctionalTestingFramework/Test/Handlers/CestObjectHandler.php new file mode 100644 index 000000000..0bb21897a --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/CestObjectHandler.php @@ -0,0 +1,99 @@ +initCestData(); + } + + return self::$cestObjectHandler; + } + + /** + * CestObjectHandler constructor. + */ + private function __construct() + { + // private constructor + } + + /** + * Takes a cest name and returns the corresponding cest. + * + * @param string $cestName + * @return CestObject + */ + public function getObject($cestName) + { + return $this->cests[$cestName]; + } + + /** + * Returns all cests parsed from xml indexed by cestName. + * + * @return array + */ + public function getAllObjects() + { + return $this->cests; + } + + /** + * This method reads all Cest.xml files into objects and stores them in an array for future access. + * + * @return void + */ + private function initCestData() + { + $testDataParser = ObjectManagerFactory::getObjectManager()->create(TestDataParser::class); + $parsedCestArray = $testDataParser->readTestData(); + + $cestObjectExtractor = new CestObjectExtractor(); + + foreach ($parsedCestArray[CestObjectHandler::XML_ROOT] as $cestName => $cestData) { + if (!is_array($cestData)) { + continue; + } + + $this->cests[$cestName] = $cestObjectExtractor->extractCest($cestData); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php new file mode 100644 index 000000000..df30cc213 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php @@ -0,0 +1,138 @@ +name = $name; + $this->arguments = $arguments; + $this->parsedActions = $actions; + } + + /** + * Gets the ordered steps including merged waits + * + * @param array $arguments + * @return array + */ + public function getSteps($arguments) + { + $mergeUtil = new ActionMergeUtil(); + $args = $this->arguments; + + if ($arguments) { + $args = array_merge($args, $arguments); + } + + return $mergeUtil->mergeStepsAndInsertWaits($this->getResolvedActionsWithArgs($args)); + } + + /** + * Function which takes a set of arguments to be appended to an action objects fields returns resulting + * action objects with proper argument.field references. + * + * @param array $arguments + * @return array + */ + private function getResolvedActionsWithArgs($arguments) + { + $resolvedActions = []; + $regexPattern = '/{{([\w]+)/'; + + foreach ($this->parsedActions as $action) { + $varAttributes = array_intersect(self::VAR_ATTRIBUTES, array_keys($action->getCustomActionAttributes())); + if (!empty($varAttributes)) { + $newActionAttributes = []; + // 1 check to see if we have pertinent var + foreach ($varAttributes as $varAttribute) { + $attributeValue = $action->getCustomActionAttributes()[$varAttribute]; + preg_match_all($regexPattern, $attributeValue, $matches); + if (empty($matches[0]) & empty($matches[1])) { + continue; + } + + $newActionAttributes[$varAttribute] = $this->resolveNewAttribute( + $arguments, + $attributeValue, + $matches + ); + } + + $resolvedActions[$action->getMergeKey()] = new ActionObject( + $action->getMergeKey(), + $action->getType(), + array_merge($action->getCustomActionAttributes(), $newActionAttributes), + $action->getLinkedAction(), + $action->getOrderOffset() + ); + } else { + // add action here if we do not see any userInput in this particular action + $resolvedActions[$action->getMergeKey()] = $action; + } + } + + return $resolvedActions; + } + + /** + * Function which takes an array of arguments to use for replacement of var name, the string which contains + * the variable for replacement, an array of matching vars. + * + * @param array $arguments + * @param string $attributeValue + * @param array $matches + * @return string + */ + private function resolveNewAttribute($arguments, $attributeValue, $matches) + { + $newAttributeVal = $attributeValue; + foreach ($matches[1] as $var) { + if (array_key_exists($var, $arguments)) { + $newAttributeVal = str_replace($var, $arguments[$var], $newAttributeVal); + } + } + + return $newAttributeVal; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php new file mode 100644 index 000000000..de3e58e5c --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -0,0 +1,401 @@ +mergeKey = $mergeKey; + $this->type = $type; + $this->actionAttributes = $actionAttributes; + $this->linkedAction = $linkedAction; + + if ($order == ActionObject::MERGE_ACTION_ORDER_AFTER) { + $this->orderOffset = 1; + } + } + + /** + * This function returns the string property mergeKey. + * + * @return string + */ + public function getMergeKey() + { + return $this->mergeKey; + } + + /** + * This function returns the string property type. + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * This function returns an array of action attributes mapped by key. For example + * the tag has 3 attributes, + * only 2 of which are specific to the 'seeNumberOfElements' tag. As a result this function would + * return the array would return [selector => value1, expected => value2] + * The returned array is also the merged result of the resolved and normal actions, giving + * priority to the resolved actions (resolved selector instead of section.element, etc). + * + * @return array + */ + public function getCustomActionAttributes() + { + return array_merge($this->actionAttributes, $this->resolvedCustomAttributes); + } + + /** + * This function returns the string property linkedAction, describing a step to reference for a merge. + * + * @return string + */ + public function getLinkedAction() + { + return $this->linkedAction; + } + + /** + * This function returns the int property orderOffset, describing before or after for a merge. + * + * @return int + */ + public function getOrderOffset() + { + return $this->orderOffset; + } + + /** + * This function returns the int property timeout, this can be set as a result of the use of a section element + * requiring a wait. + * + * @return int + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Populate the resolved custom attributes array with lookup values for the following attributes: + * selector + * url + * userInput + * + * @return void + */ + public function resolveReferences() + { + if (empty($this->resolvedCustomAttributes)) { + $this->resolveSelectorReferenceAndTimeout(); + $this->resolveUrlReference(); + $this->resolveDataInputReferences(); + } + } + + /** + * Look up the selector for SomeSectionName.ElementName and set it as the selector attribute in the + * resolved custom attributes. Also set the timeout value. + * e.g. {{SomeSectionName.ElementName}} becomes #login-button + * + * @return void + */ + private function resolveSelectorReferenceAndTimeout() + { + if (!array_key_exists(ActionObject::ACTION_ATTRIBUTE_SELECTOR, $this->actionAttributes)) { + return; + } + + $selector = $this->actionAttributes[ActionObject::ACTION_ATTRIBUTE_SELECTOR]; + + $replacement = $this->findAndReplaceReferences(SectionObjectHandler::getInstance(), $selector); + if ($replacement) { + $this->resolvedCustomAttributes[ActionObject::ACTION_ATTRIBUTE_SELECTOR] = $replacement; + } + } + + /** + * Look up the url for SomePageName and set it, with MAGENTO_BASE_URL prepended, as the url attribute in the + * resolved custom attributes. + * e.g. {{SomePageName}} becomes http://localhost:76543/some/url + * + * @return void + */ + private function resolveUrlReference() + { + if (!array_key_exists(ActionObject::ACTION_ATTRIBUTE_URL, $this->actionAttributes)) { + return; + } + + $url = $this->actionAttributes[ActionObject::ACTION_ATTRIBUTE_URL]; + + $replacement = $this->findAndReplaceReferences(PageObjectHandler::getInstance(), $url); + if ($replacement) { + $this->resolvedCustomAttributes[ActionObject::ACTION_ATTRIBUTE_URL] = $replacement; + } + } + + /** + * Look up the value for EntityDataObjectName.Key and set it as the corresponding attribute in the resolved custom + * attributes. + * e.g. {{CustomerEntityFoo.FirstName}} becomes Jerry + * + * @return void + */ + private function resolveDataInputReferences() + { + $actionAttributeKeys = array_keys($this->actionAttributes); + $relevantDataAttributes = array_intersect($actionAttributeKeys, ActionObject::DATA_ENABLED_ATTRIBUTES); + + if (empty($relevantDataAttributes)) { + return; + } + + foreach ($relevantDataAttributes as $dataAttribute) { + $varInput = $this->actionAttributes[$dataAttribute]; + $replacement = $this->findAndReplaceReferences(DataObjectHandler::getInstance(), $varInput); + if ($replacement) { + $this->resolvedCustomAttributes[$dataAttribute] = $replacement; + } + } + } + + /** + * Return an array containing the name (before the period) and key (after the period) in a {{reference.foo}}. + * Also truncates variables inside parenthesis. + * + * @param string $reference + * @return string[] The name and key that is referenced. + */ + private function stripAndSplitReference($reference) + { + $strippedReference = str_replace('}}', '', str_replace('{{', '', $reference)); + $strippedReference = preg_replace( + ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER, + '', + $strippedReference + ); + return explode('.', $strippedReference); + } + + /** + * Returns an array containing all parameters found inside () block of test input. + * Returns null if no parameters were found. + * + * @param string $reference + * @return array|null + */ + private function stripAndReturnParameters($reference) + { + preg_match(ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER, $reference, $matches); + if (!empty($matches)) { + $strippedReference = str_replace(')', '', str_replace('(', '', $matches[0])); + return explode(',', $strippedReference); + } + return null; + } + + /** + * Return a string based on a reference to a page, section, or data field (e.g. {{foo.ref}} resolves to 'data') + * + * @param ObjectHandlerInterface $objectHandler + * @param string $inputString + * @return string | null + * @throws \Exception + */ + private function findAndReplaceReferences($objectHandler, $inputString) + { + //Determine if there are Parethesis and parameters. If not, use strict regex. If so, use nested regex. + preg_match_all(ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER, $inputString, $variableMatches); + if (empty($variableMatches[0])) { + $regex = ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN; + } else { + $regex = ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_NESTED; + } + preg_match_all($regex, $inputString, $matches); + + if (empty($matches[0])) { + return $inputString; + } + + $outputString = $inputString; + + foreach ($matches[0] as $match) { + $replacement = null; + $parameterized = false; + list($objName) = $this->stripAndSplitReference($match); + + $obj = $objectHandler->getObject($objName); + + // specify behavior depending on field + switch (get_class($obj)) { + case PageObject::class: + $replacement = $obj->getUrl(); + $parameterized = $obj->isParameterized(); + break; + case SectionObject::class: + list(,$objField) = $this->stripAndSplitReference($match); + $parameterized = $obj->getElement($objField)->isParameterized(); + $replacement = $obj->getElement($objField)->getLocator(); + break; + case (get_class($obj) == EntityDataObject::class): + list(,$objField) = $this->stripAndSplitReference($match); + + if (strpos($objField, '[') == true) { + // Access ... + $parts = explode('[', $objField); + $name = $parts[0]; + $index = str_replace(']', '', $parts[1]); + $replacement = $obj->getDataByName( + $name, + EntityDataObject::CEST_UNIQUE_NOTATION + )[$index]; + } else { + // Access + $replacement = $obj->getDataByName($objField, EntityDataObject::CEST_UNIQUE_NOTATION); + } + break; + } + + if ($replacement == null && get_class($objectHandler) != DataObjectHandler::class) { + return $this->findAndReplaceReferences(DataObjectHandler::getInstance(), $outputString); + } elseif ($replacement == null) { + throw new \Exception("Could not resolve entity reference " . $inputString); + } + + //If Page or Section's Element is has parameterized = true attribute, attempt to do parameter replacement. + if ($parameterized) { + $parameterList = $this->stripAndReturnParameters($match); + $replacement = $this->matchParameterReferences($replacement, $parameterList); + } + $outputString = str_replace($match, $replacement, $outputString); + } + return $outputString; + } + + /** + * Finds all {{var}} occurrences in reference, and replaces them in sequence with parameters list given. + * Parameter list given is also resolved, attempting to match {{data.field}} references. + * + * @param string $reference + * @param array $parameters + * @return string + * @throws \Exception + */ + private function matchParameterReferences($reference, $parameters) + { + preg_match_all('/{{[\w.]+}}/', $reference, $varMatches); + if (count($varMatches[0]) > count($parameters)) { + throw new \Exception( + "Parameter Resolution Failed: Not enough parameters given for reference " . + $reference . ". Parameters Given: " . implode(",", $parameters) + ); + } elseif (count($varMatches[0]) < count($parameters)) { + throw new \Exception( + "Parameter Resolution Failed: Too many parameters given for reference " . + $reference . ". Parameters Given: " . implode(",", $parameters) + ); + } + + //Attempt to Resolve {{data}} references to actual output. + $resolvedParameters = []; + foreach ($parameters as $parameter) { + $resolvedParameters[] = $this->findAndReplaceReferences( + DataObjectHandler::getInstance(), + $parameter + ); + } + + $resolveIndex = 0; + foreach ($varMatches[0] as $var) { + $reference = str_replace($var, $resolvedParameters[$resolveIndex++], $reference); + } + return $reference; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/CestHookObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/CestHookObject.php new file mode 100644 index 000000000..3837aaf69 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/CestHookObject.php @@ -0,0 +1,75 @@ +type = $type; + $this->actions = $actions; + $this->customData = $customData; + } + + /** + * Getter for hook type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Returns an array of action objects to be executed within the hook. + * + * @return array + */ + public function getActions() + { + return $this->actions; + } + + /** + * Returns an array of customData to be interperpreted by the generator. + * @return array|null + */ + public function getCustomData() + { + return $this->customData; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/CestObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/CestObject.php new file mode 100644 index 000000000..5b7018657 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/CestObject.php @@ -0,0 +1,96 @@ +name = $name; + $this->annotations = $annotations; + $this->tests = $tests; + $this->hooks = $hooks; + } + + /** + * Returns name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns annotations. + * + * @return array + */ + public function getAnnotations() + { + return $this->annotations; + } + + /** + * Returns tests. + * + * @return array + */ + public function getTests() + { + return $this->tests; + } + + /** + * Returns hooks. + * + * @return array + */ + public function getHooks() + { + return $this->hooks; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php new file mode 100644 index 000000000..d4b79075f --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php @@ -0,0 +1,132 @@ +name = $name; + $this->parsedSteps = $parsedSteps; + $this->annotations = $annotations; + $this->customData = $customData; + } + + /** + * Getter for the Test Name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Getter for the Test Annotations + * + * @return array + */ + public function getAnnotations() + { + return $this->annotations; + } + + /** + * Getter for the custom data + * @return array|null + */ + public function getCustomData() + { + return $this->customData; + } + + /** + * This method calls a function to merge custom steps and returns the resulting ordered set of steps. + * + * @return array + */ + public function getOrderedActions() + { + $mergeUtil = new ActionMergeUtil(); + $mergedSteps = $mergeUtil->mergeStepsAndInsertWaits($this->parsedSteps); + return $this->extractActionGroups($mergedSteps); + } + + /** + * Method to insert action group references into step flow + * + * @param array $mergedSteps + * @return array + */ + private function extractActionGroups($mergedSteps) + { + $newOrderedList = []; + + foreach ($mergedSteps as $key => $mergedStep) { + /**@var ActionObject $mergedStep**/ + if ($mergedStep->getType() == ActionObjectExtractor::ACTION_GROUP_TAG) { + $actionGroup = ActionGroupObjectHandler::getInstance()->getObject( + $mergedStep->getCustomActionAttributes()[ActionObjectExtractor::ACTION_GROUP_REF] + ); + $args = $mergedStep->getCustomActionAttributes()[ActionObjectExtractor::ACTION_GROUP_ARGUMENTS] ?? null; + $actionsToMerge = $actionGroup->getSteps($args); + $newOrderedList = $newOrderedList + $actionsToMerge; + } else { + $newOrderedList[$key] = $mergedStep; + } + } + + return $newOrderedList; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Parsers/ActionGroupDataParser.php b/src/Magento/FunctionalTestingFramework/Test/Parsers/ActionGroupDataParser.php new file mode 100644 index 000000000..4709d3aa8 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Parsers/ActionGroupDataParser.php @@ -0,0 +1,35 @@ +actionGroupData = $actionGroupData; + } + + /** + * Read action group xml and return as an array. + * + * @return array + */ + public function readActionGroupData() + { + return $this->actionGroupData->get(); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Parsers/TestDataParser.php b/src/Magento/FunctionalTestingFramework/Test/Parsers/TestDataParser.php new file mode 100644 index 000000000..d78e737b7 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Parsers/TestDataParser.php @@ -0,0 +1,35 @@ +testData = $testData; + } + + /** + * Returns an array of data based on *Cest.xml files + * + * @return array + */ + public function readTestData() + { + return $this->testData->get(); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php new file mode 100644 index 000000000..5b4f45d2a --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php @@ -0,0 +1,85 @@ +actionObjectExtractor = new ActionObjectExtractor(); + } + + /** + * Method to parse array of action group data into ActionGroupObject + * + * @param array $actionGroupData + * @return ActionGroupObject + */ + public function extractActionGroup($actionGroupData) + { + $arguments = []; + + $actionData = $this->stripDescriptorTags( + $actionGroupData, + self::NODE_NAME, + self::ACTION_GROUP_ARGUMENTS, + self::NAME + ); + + $actions = $this->actionObjectExtractor->extractActions($actionData); + + if (array_key_exists(self::ACTION_GROUP_ARGUMENTS, $actionGroupData)) { + $arguments = $this->extractArguments($actionGroupData[self::ACTION_GROUP_ARGUMENTS]); + } + + return new ActionGroupObject( + $actionGroupData[self::NAME], + $arguments, + $actions + ); + } + + /** + * Method which extract argument declarations from an action group and returns an array of default values indexed + * by argument name. + * + * @param array $arguments + * @return array + */ + private function extractArguments($arguments) + { + $parsedArguments = []; + $argData = $this->stripDescriptorTags( + $arguments, + self::NODE_NAME + ); + + foreach ($argData as $argName => $argValue) { + $parsedArguments[$argName] = $argValue[self::DEFAULT_VALUE]; + } + + return $parsedArguments; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php new file mode 100644 index 000000000..92ee032b3 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php @@ -0,0 +1,160 @@ +mergeActions($parsedSteps); + $this->insertWaits(); + + return $this->orderedSteps; + } + + /** + * This method runs a step sort, loops steps which need to be merged, and runs the mergeStep function on each one. + * + * @param array $parsedSteps + * @return void + */ + private function mergeActions($parsedSteps) + { + $this->sortActions($parsedSteps); + + foreach ($this->stepsToMerge as $stepName => $stepToMerge) { + if (!array_key_exists($stepName, $this->orderedSteps)) { + $this->mergeAction($stepToMerge); + } + } + unset($stepName); + unset($stepToMerge); + } + + /** + * Runs through the prepared orderedSteps and calls insertWait if a step requires a wait after it. + * + * @return void + */ + private function insertWaits() + { + foreach ($this->orderedSteps as $step) { + if ($step->getTimeout()) { + $waitStepAttributes = [self::WAIT_ATTR => $step->getTimeout()]; + $waitStep = new ActionObject( + $step->getMergeKey() . self::WAIT_ACTION_SUFFIX, + self::WAIT_ACTION_NAME, + $waitStepAttributes, + $step->getMergeKey(), + self::DEFAULT_WAIT_ORDER + ); + $this->insertStep($waitStep); + } + } + } + + /** + * This method takes the steps from the parser and splits steps which need merge from steps that are ordered. + * + * @param array $parsedSteps + * @return void + * @throws XmlException + */ + private function sortActions($parsedSteps) + { + foreach ($parsedSteps as $parsedStep) { + $parsedStep->resolveReferences(); + if ($parsedStep->getLinkedAction()) { + $this->stepsToMerge[$parsedStep->getMergeKey()] = $parsedStep; + } else { + $this->orderedSteps[$parsedStep->getMergeKey()] = $parsedStep; + } + } + } + + /** + * Recursively merges in each step and its dependencies + * + * @param ActionObject $stepToMerge + * @throws XmlException + * @return void + */ + private function mergeAction($stepToMerge) + { + $linkedStep = $stepToMerge->getLinkedAction(); + + if (!array_key_exists($linkedStep, $this->orderedSteps) + and + !array_key_exists($linkedStep, $this->stepsToMerge)) { + throw new XmlException(sprintf( + self::STEP_MISSING_ERROR_MSG, + $this->getName(), + $stepToMerge->getMergeKey(), + $linkedStep + )); + } elseif (!array_key_exists($linkedStep, $this->orderedSteps)) { + $this->mergeAction($this->stepsToMerge[$linkedStep]); + } + + $this->insertStep($stepToMerge); + } + + /** + * Inserts a step into the ordered steps array based on position and step referenced. + * + * @param ActionObject $stepToMerge + * @return void + */ + private function insertStep($stepToMerge) + { + $position = array_search( + $stepToMerge->getLinkedAction(), + array_keys($this->orderedSteps) + ) + $stepToMerge->getOrderOffset(); + $previous_items = array_slice($this->orderedSteps, 0, $position, true); + $next_items = array_slice($this->orderedSteps, $position, null, true); + $this->orderedSteps = $previous_items + [$stepToMerge->getMergeKey() => $stepToMerge] + $next_items; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php new file mode 100644 index 000000000..4f75f5db8 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php @@ -0,0 +1,134 @@ + $actionData) { + $mergeKey = $actionData[self::TEST_STEP_MERGE_KEY]; + if ($actionData[self::NODE_NAME] === TestEntityExtractor::TEST_STEP_ENTITY_CREATION) { + $actionData = $this->stripDataFields($actionData); + } + + $actionAttributes = $this->stripDescriptorTags( + $actionData, + self::TEST_STEP_MERGE_KEY, + self::NODE_NAME + ); + $linkedAction = null; + $order = null; + + if ($actionData[self::NODE_NAME] === self::ACTION_GROUP_TAG) { + $actionAttributes = $this->processActionGroupArgs($actionAttributes); + } + + if (array_key_exists(self::TEST_ACTION_BEFORE, $actionData) + and array_key_exists(self::TEST_ACTION_AFTER, $actionData)) { + throw new XmlException(sprintf(self::BEFORE_AFTER_ERROR_MSG, $actionName)); + } + + if (array_key_exists(self::TEST_ACTION_BEFORE, $actionData)) { + $linkedAction = $actionData[self::TEST_ACTION_BEFORE]; + $order = self::TEST_ACTION_BEFORE; + } elseif (array_key_exists(self::TEST_ACTION_AFTER, $actionData)) { + $linkedAction = $actionData[self::TEST_ACTION_AFTER]; + $order = self::TEST_ACTION_AFTER; + } + // TODO this is to be implemented later. Currently the schema does not use or need return var. + /*if (array_key_exists(ActionGroupObjectHandler::TEST_ACTION_RETURN_VARIABLE, $actionData)) { + $returnVariable = $actionData[ActionGroupObjectHandler::TEST_ACTION_RETURN_VARIABLE]; + }*/ + + $actions[] = new ActionObject( + $mergeKey, + $actionData[self::NODE_NAME], + $actionAttributes, + $linkedAction, + $order + ); + } + + return $actions; + } + + /** + * Takes the action group reference and parses out arguments as an array that can be passed to override defaults + * defined in the action group xml. + * + * @param array $actionAttributeData + * @return array + */ + private function processActionGroupArgs($actionAttributeData) + { + $actionAttributeArgData = []; + foreach ($actionAttributeData as $attributeDataKey => $attributeDataValues) { + if ($attributeDataKey == self::ACTION_GROUP_REF) { + $actionAttributeArgData[self::ACTION_GROUP_REF] = $attributeDataValues; + continue; + } + + $actionAttributeArgData[self::ACTION_GROUP_ARGUMENTS][$attributeDataKey] = + $attributeDataValues[self::ACTION_GROUP_ARG_VALUE]; + } + + return $actionAttributeArgData; + } + + /** + * Function which checks an entity definition for type array and strips this key out (as data is not stores in this + * type of object). + * + * @param array $entityDataArray + * @return array + */ + private function stripDataFields($entityDataArray) + { + $results = $entityDataArray; + foreach ($entityDataArray as $key => $attribute) { + if (is_array($attribute)) { + unset($results[$key]); + } + } + + return $results; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php new file mode 100644 index 000000000..be9ce9cbe --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php @@ -0,0 +1,48 @@ +stripDescriptorTags($cestAnnotations, self::NODE_NAME); + + // parse the Cest annotations + foreach ($annotations as $annotationKey => $annotationData) { + $annotationValues = []; + foreach ($annotationData as $annotationValue) { + $annotationValues[] = $annotationValue[self::ANNOTATION_VALUE]; + } + + $annotationObjects[$annotationKey] = $annotationValues; + } + + return $annotationObjects; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/BaseCestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/BaseCestObjectExtractor.php new file mode 100644 index 000000000..41b0c3439 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/BaseCestObjectExtractor.php @@ -0,0 +1,44 @@ +actionObjectExtractor = new ActionObjectExtractor(); + $this->testEntityExtractor = new TestEntityExtractor(); + } + + /** + * This method trims all irrelevant tags to extract hook information including before and after tags + * and their relevant actions. The result is an array of CestHookObjects. + * + * @param string $hookType + * @param array $cestHook + * @return CestHookObject + */ + public function extractHook($hookType, $cestHook) + { + $hookActions = $this->stripDescriptorTags( + $cestHook, + self::NODE_NAME + ); + + $hook = new CestHookObject( + $hookType, + $this->actionObjectExtractor->extractActions($hookActions), + $this->testEntityExtractor->extractTestEntities($hookActions) + ); + + return $hook; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/CestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/CestObjectExtractor.php new file mode 100644 index 000000000..7582d520e --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/CestObjectExtractor.php @@ -0,0 +1,82 @@ +stripDescriptorTags( + $cestData, + self::NODE_NAME, + self::NAME + ); + + if (array_key_exists(self::CEST_BEFORE_HOOK, $cestData)) { + $hooks[self::CEST_BEFORE_HOOK] = $cestHookObjectExtractor->extractHook( + self::CEST_BEFORE_HOOK, + $cestData[self::CEST_BEFORE_HOOK] + ); + + $tests = $this->stripDescriptorTags($tests, self::CEST_BEFORE_HOOK); + } + + if (array_key_exists(self::CEST_AFTER_HOOK, $cestData)) { + $hooks[self::CEST_AFTER_HOOK] = $cestHookObjectExtractor->extractHook( + self::CEST_AFTER_HOOK, + $cestData[self::CEST_AFTER_HOOK] + ); + + $tests = $this->stripDescriptorTags($tests, self::CEST_AFTER_HOOK); + } + + if (array_key_exists(self::CEST_ANNOTATIONS, $cestData)) { + $annotations = $annotationExtractor->extractAnnotations($cestData[self::CEST_ANNOTATIONS]); + + $tests = $this->stripDescriptorTags($tests, self::CEST_ANNOTATIONS); + } + + return new CestObject( + $cestData[self::NAME], + $annotations, + $testObjectExtractor->extractTestData($tests), + $hooks + ); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestEntityExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestEntityExtractor.php new file mode 100644 index 000000000..cd72af604 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestEntityExtractor.php @@ -0,0 +1,57 @@ + $actionData) { + $entityData = []; + if ($actionData[TestEntityExtractor::NODE_NAME] === TestEntityExtractor::TEST_STEP_ENTITY_CREATION) { + foreach ($actionData as $key => $attribute) { + if (is_array($attribute)) { + $entityData[$attribute[TestEntityExtractor::TEST_ENTITY_CREATION_KEY]] + = $attribute[TestEntityExtractor::TEST_ENTITY_CREATION_VALUE]; + unset($actionData[$key]); + } + } + $testEntities[$actionData[TestEntityExtractor::NAME]] = $entityData; + } + } + + return $testEntities; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php new file mode 100644 index 000000000..2948059e2 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php @@ -0,0 +1,88 @@ +actionObjectExtractor = new ActionObjectExtractor(); + $this->annotationExtractor = new AnnotationExtractor(); + $this->testEntityExtractor = new TestEntityExtractor(); + } + + /** + * This method takes and array of test data and strips away irrelevant tags. The data is converted into an array of + * TestObjects. + * + * @param array $cestTestData + * @return array + */ + public function extractTestData($cestTestData) + { + $testObjects = []; + + // parse the tests + foreach ($cestTestData as $testName => $testData) { + if (!is_array($testData)) { + continue; + } + + $testAnnotations = []; + $testActions = $this->stripDescriptorTags( + $testData, + self::NODE_NAME, + self::NAME, + self::TEST_ANNOTATIONS + ); + + if (array_key_exists(self::TEST_ANNOTATIONS, $testData)) { + $testAnnotations = $this->annotationExtractor->extractAnnotations($testData[self::TEST_ANNOTATIONS]); + } + + $testObjects[] = new TestObject( + $testName, + $this->actionObjectExtractor->extractActions($testActions), + $testAnnotations, + $this->testEntityExtractor->extractTestEntities($testActions) + ); + } + + return $testObjects; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/sampleActionGroup.xml b/src/Magento/FunctionalTestingFramework/Test/etc/sampleActionGroup.xml new file mode 100644 index 000000000..285be1f58 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/etc/sampleActionGroup.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/sampleCest.xml b/src/Magento/FunctionalTestingFramework/Test/etc/sampleCest.xml new file mode 100644 index 000000000..8bcec49ae --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/etc/sampleCest.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd new file mode 100644 index 000000000..945f2e133 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd @@ -0,0 +1,1479 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Magento/FunctionalTestingFramework/Util/ApiClientUtil.php b/src/Magento/FunctionalTestingFramework/Util/ApiClientUtil.php new file mode 100644 index 000000000..0db7075b8 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/ApiClientUtil.php @@ -0,0 +1,117 @@ +apiPath = $apiPath; + $this->headers = $headers; + $this->apiOperation = $apiOperation; + $this->jsonBody = $jsonBody; + + $this->curl = curl_init(); + } + + /** + * Submits the request based on object properties + * + * @param bool $verbose + * @return string|bool + * @throws \Exception + */ + public function submit($verbose = false) + { + $url = null; + + if ($this->jsonBody) { + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $this->jsonBody); + } + + curl_setopt($this->curl, CURLOPT_VERBOSE, $verbose); + + if ((getenv('MAGENTO_RESTAPI_SERVER_HOST') !== false) + && (getenv('MAGENTO_RESTAPI_SERVER_HOST') !== '') ) { + $url = getenv('MAGENTO_RESTAPI_SERVER_HOST'); + } else { + $url = getenv('MAGENTO_BASE_URL'); + } + + if ((getenv('MAGENTO_RESTAPI_SERVER_PORT') !== false) + && (getenv('MAGENTO_RESTAPI_SERVER_PORT') !== '')) { + $url .= ':' . getenv('MAGENTO_RESTAPI_SERVER_PORT'); + } + + curl_setopt_array($this->curl, [ + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_HTTPHEADER => $this->headers, + CURLOPT_CUSTOMREQUEST => $this->apiOperation, + CURLOPT_URL => $url . $this->apiPath + ]); + + $response = curl_exec($this->curl); + $http_code = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + + if ($response === false || !in_array($http_code, ApiClientUtil::SUCCESSFUL_HTTP_CODES)) { + throw new \Exception('API returned response code: ' . $http_code . ' Response:' . $response); + } + + curl_close($this->curl); + + return $response; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php b/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php new file mode 100644 index 000000000..0b95a4ca7 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php @@ -0,0 +1,133 @@ +data); + if (!$this->isValid()) { + $this->next(); + } + } + + /** + * Seek to next valid row + * + * @return void + */ + public function next() + { + $this->current = next($this->data); + + if ($this->current !== false) { + if (!$this->isValid()) { + $this->next(); + } + } else { + $this->key = null; + } + } + + /** + * Check if current position is valid + * + * @return boolean + */ + public function valid() + { + $current = current($this->data); + if ($current === false || $current === null) { + return false; + } else { + return true; + } + } + + /** + * Get data key of the current data element + * + * @return int|string + */ + public function key() + { + return key($this->data); + } + + /** + * To make iterator countable + * + * @return int + */ + public function count() + { + return count($this->data); + } + + /** + * Initialize first element + * + * @return void + */ + protected function initFirstElement() + { + if ($this->data) { + $this->current = reset($this->data); + if (!$this->isValid()) { + $this->next(); + } + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Iterator/File.php b/src/Magento/FunctionalTestingFramework/Util/Iterator/File.php new file mode 100644 index 000000000..e019e5683 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Iterator/File.php @@ -0,0 +1,56 @@ +data = $paths; + $this->initFirstElement(); + } + + /** + * Get file content + * + * @return string + */ + public function current() + { + if (!isset($this->cached[$this->current])) { + $this->cached[$this->current] = file_get_contents($this->current); + } + return $this->cached[$this->current]; + + } + + /** + * Check if current element is valid + * + * @return boolean + */ + protected function isValid() + { + return true; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php new file mode 100644 index 000000000..19c541f10 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php @@ -0,0 +1,234 @@ + 1]; + + /** + * ModuleResolver instance. + * + * @var ModuleResolver + */ + private static $instance = null; + + /** + * SequenceSorter instance. + * + * @var ModuleResolver\SequenceSorterInterface + */ + protected $sequenceSorter; + + /** + * Get ModuleResolver instance. + * + * @return ModuleResolver + */ + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new ModuleResolver(); + } + return self::$instance; + } + + /** + * ModuleResolver constructor. + */ + private function __construct() + { + $objectManager = \Magento\FunctionalTestingFramework\ObjectManagerFactory::getObjectManager(); + $this->sequenceSorter = $objectManager->get( + \Magento\FunctionalTestingFramework\Util\ModuleResolver\SequenceSorterInterface::class + ); + } + + /** + * Return an array of enabled modules of target Magento instance. + * + * @return array + */ + public function getEnabledModules() + { + if (isset($this->enabledModules)) { + return $this->enabledModules; + } + + $token = $this->getAdminToken(); + if (!$token || !is_string($token)) { + $this->enabledModules = []; + return $this->enabledModules; + } + + $url = $_ENV['MAGENTO_BASE_URL'] . $this->moduleUrl; + + $headers = [ + 'Authorization: Bearer ' . $token, + ]; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + $response = curl_exec($ch); + + if (!$response) { + $this->enabledModules = []; + } else { + $this->enabledModules = json_decode($response); + } + return $this->enabledModules; + } + + /** + * Return an array of module whitelist that not exist in target Magento instance. + * + * @return array + */ + protected function getModuleWhitelist() + { + $moduleWhitelist = getenv(self::MODULE_WHITELIST); + + if (empty($moduleWhitelist)) { + return []; + } + return array_map('trim', explode(',', $moduleWhitelist)); + } + + /** + * Return the modules path based on which modules are enabled in the target Magento instance. + * + * @return array + */ + public function getModulesPath() + { + if (isset($this->enabledModulePaths)) { + return $this->enabledModulePaths; + } + + $enabledModules = $this->getEnabledModules(); + $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; + $allModulePaths = glob($modulePath . '*/*'); + if (empty($enabledModules)) { + $this->enabledModulePaths = $allModulePaths; + return $this->enabledModulePaths; + } + + $enabledModules = array_merge($enabledModules, $this->getModuleWhitelist()); + $enabledDirectories = []; + foreach ($enabledModules as $module) { + $directoryName = explode('_', $module)[1]; + $enabledDirectories[$directoryName] = $directoryName; + } + + foreach ($allModulePaths as $index => $modulePath) { + $moduleShortName = basename($modulePath); + if (!isset($enabledDirectories[$moduleShortName]) && !isset($this->knownDirectories[$moduleShortName])) { + unset($allModulePaths[$index]); + } + } + + $this->enabledModulePaths = $allModulePaths; + return $this->enabledModulePaths; + } + + /** + * Get the API token for admin. + * + * @return string|bool + */ + protected function getAdminToken() + { + $login = $_ENV['MAGENTO_ADMIN_USERNAME']; + $password = $_ENV['MAGENTO_ADMIN_PASSWORD']; + if (!$login || !$password || !isset($_ENV['MAGENTO_BASE_URL'])) { + return false; + } + + $url = $_ENV['MAGENTO_BASE_URL'] . $this->adminTokenUrl; + $data = [ + 'username' => $login, + 'password' => $password + ]; + $headers = [ + 'Content-Type: application/json', + ]; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + $response = curl_exec($ch); + if (!$response) { + return $response; + } + return json_decode($response); + } + + /** + * Sort files according module sequence. + * + * @param array $files + * @return array + */ + public function sortFilesByModuleSequence(array $files) + { + return $this->sequenceSorter->sort($files); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver/SequenceSorter.php b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver/SequenceSorter.php new file mode 100644 index 000000000..c8a0d7530 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver/SequenceSorter.php @@ -0,0 +1,19 @@ +getAllObjects(); + } + + /** + * Create a single PHP file containing the $cestPhp using the $filename. + * If the _generated directory doesn't exist it will be created. + * + * @param string $cestPhp + * @param string $filename + * @return void + * @throws \Exception + */ + private function createCestFile($cestPhp, $filename) + { + $exportDirectory = TESTS_MODULE_PATH . "/_generated"; + $exportFilePath = sprintf("%s/%s.php", $exportDirectory, $filename); + + if (!is_dir($exportDirectory)) { + mkdir($exportDirectory, 0777, true); + } + + $file = fopen($exportFilePath, 'w'); + + if (!$file) { + throw new \Exception("Could not open the file!"); + } + + fwrite($file, $cestPhp); + fclose($file); + } + + /** + * Assemble ALL PHP strings using the assembleAllCestPhp function. Loop over and pass each array item + * to the createCestFile function. + * + * @return void + */ + public function createAllCestFiles() + { + $cestPhpArray = $this->assembleAllCestPhp(); + + foreach ($cestPhpArray as $cestPhpFile) { + $this->createCestFile($cestPhpFile[1], $cestPhpFile[0]); + } + } + + /** + * Assemble the entire PHP string for a single Test based on a Cest Object. + * Create all of the PHP strings for a Test. Concatenate the strings together. + * + * @param \Magento\FunctionalTestingFramework\Test\Objects\CestObject $cestObject + * @return string + */ + private function assembleCestPhp($cestObject) + { + $usePhp = $this->generateUseStatementsPhp($cestObject); + $classAnnotationsPhp = $this->generateClassAnnotationsPhp($cestObject->getAnnotations()); + $className = $cestObject->getName(); + $className = str_replace(' ', '', $className); + $hookPhp = $this->generateHooksPhp($cestObject->getHooks()); + $testsPhp = $this->generateTestsPhp($cestObject->getTests()); + + $cestPhp = "loadAllCestObjects(); + $cestPhpArray = []; + + foreach ($cestObjects as $cest) { + $name = $cest->getName(); + $name = $string = str_replace(' ', '', $name); + $php = $this->assembleCestPhp($cest); + $cestPhpArray[] = [$name, $php]; + } + + return $cestPhpArray; + } + + /** + * Creates a PHP string for the necessary Allure and AcceptanceTester use statements. + * Since we don't support other dependencies at this time, this function takes no parameter. + * + * @return string + */ + private function generateUseStatementsPhp() + { + $useStatementsPhp = "use Magento\FunctionalTestingFramework\AcceptanceTester;\n"; + + $useStatementsPhp .= "use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler;\n"; + $useStatementsPhp .= "use Magento\FunctionalTestingFramework\DataGenerator\Api\EntityApiHandler;\n"; + $useStatementsPhp .= "use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject;\n"; + + $allureStatements = [ + "Yandex\Allure\Adapter\Annotation\Features;", + "Yandex\Allure\Adapter\Annotation\Stories;", + "Yandex\Allure\Adapter\Annotation\Title;", + "Yandex\Allure\Adapter\Annotation\Description;", + "Yandex\Allure\Adapter\Annotation\Parameter;", + "Yandex\Allure\Adapter\Annotation\Severity;", + "Yandex\Allure\Adapter\Model\SeverityLevel;", + "Yandex\Allure\Adapter\Annotation\TestCaseId;\n" + ]; + + foreach ($allureStatements as $allureUseStatement) { + $useStatementsPhp .= sprintf("use %s\n", $allureUseStatement); + } + + return $useStatementsPhp; + } + + /** + * Creates a PHP string for the Class Annotations block if the Cest file contains an block, outside + * of the blocks. + * + * @param array $classAnnotationsObject + * @return string + */ + private function generateClassAnnotationsPhp($classAnnotationsObject) + { + $classAnnotationsPhp = "/**\n"; + + foreach ($classAnnotationsObject as $annotationType => $annotationName) { + if ($annotationType == "features") { + $features = ""; + + foreach ($annotationName as $name) { + $features .= sprintf("\"%s\"", $name); + + if (next($annotationName)) { + $features .= ", "; + } + } + + $classAnnotationsPhp .= sprintf(" * @Features({%s})\n", $features); + } + + if ($annotationType == "stories") { + $stories = ""; + + foreach ($annotationName as $name) { + $stories .= sprintf("\"%s\"", $name); + + if (next($annotationName)) { + $stories .= ", "; + } + } + + $classAnnotationsPhp .= sprintf(" * @Stories({%s})\n", $stories); + } + + if ($annotationType == "title") { + $classAnnotationsPhp .= sprintf( + " * @Title(\"%s\")\n", + ucwords($annotationType), + $annotationName[0] + ); + } + + if ($annotationType == "description") { + $classAnnotationsPhp .= sprintf(" * @Description(\"%s\")\n", $annotationName[0]); + } + + if ($annotationType == "severity") { + $classAnnotationsPhp .= sprintf(" * @Severity(level = SeverityLevel::%s)\n", $annotationName[0]); + } + + if ($annotationType == "testCaseId") { + $classAnnotationsPhp .= sprintf(" * TestCaseId(\"%s\")\n", $annotationName[0]); + } + + if ($annotationType == "group") { + foreach ($annotationName as $group) { + $classAnnotationsPhp .= sprintf(" * @group %s\n", $group); + } + } + + if ($annotationType == "env") { + foreach ($annotationName as $env) { + $classAnnotationsPhp .= sprintf(" * @env %s\n", $env); + } + } + } + + $classAnnotationsPhp .= " */\n"; + + return $classAnnotationsPhp; + } + + /** + * Creates a PHP string for the actions contained withing a block. + * Since nearly half of all Codeception methods don't share the same signature I had to setup a massive Case + * statement to handle each unique action. At the bottom of the case statement there is a generic function that can + * construct the PHP string for nearly half of all Codeception actions. + * @param array $stepsObject + * @param array $stepsData + * @param array|bool $hookObject + * @return string + */ + private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) + { + $testSteps = ""; + + foreach ($stepsObject as $steps) { + $actor = "I"; + $actionName = $steps->getType(); + $customActionAttributes = $steps->getCustomActionAttributes(); + $selector = null; + $selector1 = null; + $selector2 = null; + $input = null; + $parameterArray = null; + $returnVariable = null; + $x = null; + $y = null; + $html = null; + $url = null; + $function = null; + $time = null; + $locale = null; + $username = null; + $password = null; + $width = null; + $height = null; + $requiredAction = null; + $value = null; + $button = null; + $parameter = null; + + if (isset($customActionAttributes['returnVariable'])) { + $returnVariable = $customActionAttributes['returnVariable']; + } + + if (isset($customActionAttributes['variable'])) { + $input = $this->addDollarSign($customActionAttributes['variable']); + } elseif (isset($customActionAttributes['userInput']) && isset($customActionAttributes['url'])) { + $input = $this->addUniquenessFunctionCall($customActionAttributes['userInput']); + $url = $this->addUniquenessFunctionCall($customActionAttributes['url']); + } elseif (isset($customActionAttributes['userInput'])) { + $input = $this->addUniquenessFunctionCall($customActionAttributes['userInput']); + } elseif (isset($customActionAttributes['url'])) { + $input = $this->addUniquenessFunctionCall($customActionAttributes['url']); + } + + if (isset($customActionAttributes['time'])) { + $time = $customActionAttributes['time']; + } + + if (isset($customActionAttributes['timeout'])) { + $time = $customActionAttributes['timeout']; + } + + if (isset($customActionAttributes['parameterArray'])) { + $paramsWithUniqueness = []; + $params = explode( + ',', + $this->stripWrappedQuotes(rtrim(ltrim($customActionAttributes['parameterArray'], '['), ']')) + ); + foreach ($params as $param) { + $paramsWithUniqueness[] = $this->addUniquenessFunctionCall($param); + } + $parameterArray = '[' . implode(',', $paramsWithUniqueness) .']'; + } + + if (isset($customActionAttributes['requiredAction'])) { + $requiredAction = $customActionAttributes['requiredAction']; + } + + if (isset($customActionAttributes['selectorArray'])) { + $selector = $customActionAttributes['selectorArray']; + } elseif (isset($customActionAttributes['selector'])) { + $selector = $this->wrapWithSingleQuotes($customActionAttributes['selector']); + } + + if (isset($customActionAttributes['selector1'])) { + $selector1 = $this->wrapWithSingleQuotes($customActionAttributes['selector1']); + } + + if (isset($customActionAttributes['selector2'])) { + $selector2 = $this->wrapWithSingleQuotes($customActionAttributes['selector2']); + } + + if (isset($customActionAttributes['x'])) { + $x = $customActionAttributes['x']; + } + + if (isset($customActionAttributes['y'])) { + $y = $customActionAttributes['y']; + } + + if (isset($customActionAttributes['function'])) { + $function = $customActionAttributes['function']; + } + + if (isset($customActionAttributes['html'])) { + $html = $customActionAttributes['html']; + } + + if (isset($customActionAttributes['locale'])) { + $locale = $this->wrapWithSingleQuotes($customActionAttributes['locale']); + } + + if (isset($customActionAttributes['username'])) { + $username = $this->wrapWithSingleQuotes($customActionAttributes['username']); + } + + if (isset($customActionAttributes['password'])) { + $password = $this->wrapWithSingleQuotes($customActionAttributes['password']); + } + + if (isset($customActionAttributes['width'])) { + $width = $customActionAttributes['width']; + } + + if (isset($customActionAttributes['height'])) { + $height = $customActionAttributes['height']; + } + + if (isset($customActionAttributes['value'])) { + $value = $this->wrapWithSingleQuotes($customActionAttributes['value']); + } + + if (isset($customActionAttributes['button'])) { + $button = $this->wrapWithSingleQuotes($customActionAttributes['button']); + } + + if (isset($customActionAttributes['parameter'])) { + $parameter = $this->wrapWithSingleQuotes($customActionAttributes['parameter']); + } + + switch ($actionName) { + case "createData": + $entity = $customActionAttributes['entity']; + $key = $steps->getMergeKey(); + //Add an informative statement to help the user debug test runs + $testSteps .= sprintf( + "\t\t$%s->amGoingTo(\"create entity that has the mergeKey: %s\");\n", + $actor, + $key + ); + //Get Entity from Static data. + $testSteps .= sprintf( + "\t\t$%s = DataObjectHandler::getInstance()->getObject(\"%s\");\n", + $entity, + $entity + ); + + //HookObject End-Product needs to be created in the Class/Cest scope, + //otherwise create them in the Test scope. + //Determine if there are required-entities and create array of required-entities for merging. + $requiredEntities = []; + $requiredEntityObjects = []; + foreach ($customActionAttributes as $customAttribute) { + if (is_array($customAttribute) && $customAttribute['nodeName'] = 'required-entity') { + if ($hookObject) { + $requiredEntities [] = "\$this->" . $customAttribute['name'] . "->getName() => " . + "\$this->" . $customAttribute['name'] . "->getType()"; + $requiredEntityObjects [] = '$this->' . $customAttribute['name']; + } else { + $requiredEntities [] = "\$" . $customAttribute['name'] . "->getName() => " + . "\$" . $customAttribute['name'] . "->getType()"; + $requiredEntityObjects [] = '$' . $customAttribute['name']; + } + } + } + //If required-entities are defined, reassign dataObject to not overwrite the static definition. + //Also, EntityApiHandler needs to be defined with customData array. + if (!empty($requiredEntities)) { + $testSteps .= sprintf( + "\t\t$%s = new EntityDataObject($%s->getName(), $%s->getType(), $%s->getData() + , array_merge($%s->getLinkedEntities(), [%s]), $%s->getUniquenessData());\n", + $entity, + $entity, + $entity, + $entity, + $entity, + implode(", ", $requiredEntities), + $entity + ); + + if ($hookObject) { + $testSteps .= sprintf( + "\t\t\$this->%s = new EntityApiHandler($%s, [%s]);\n", + $key, + $entity, + implode(', ', $requiredEntityObjects) + ); + $testSteps .= sprintf("\t\t\$this->%s->createEntity();\n", $key); + } else { + $testSteps .= sprintf( + "\t\t$%s = new EntityApiHandler($%s, [%s]);\n", + $key, + $entity, + implode(', ', $requiredEntityObjects) + ); + $testSteps .= sprintf("\t\t$%s->createEntity();\n", $key); + } + } else { + if ($hookObject) { + $testSteps .= sprintf( + "\t\t\$this->%s = new EntityApiHandler($%s);\n", + $key, + $entity + ); + $testSteps .= sprintf("\t\t\$this->%s->createEntity();\n", $key); + } else { + $testSteps .= sprintf("\t\t$%s = new EntityApiHandler($%s);\n", $key, $entity); + $testSteps .= sprintf("\t\t$%s->createEntity();\n", $key); + } + } + + break; + case "deleteData": + $key = $customActionAttributes['createDataKey']; + //Add an informative statement to help the user debug test runs + $testSteps .= sprintf( + "\t\t$%s->amGoingTo(\"delete entity that has the createDataKey: %s\");\n", + $actor, + $key + ); + + if ($hookObject) { + $testSteps .= sprintf("\t\t\$this->%s->deleteEntity();\n", $key); + } else { + $testSteps .= sprintf("\t\t$%s->deleteEntity();\n", $key); + } + break; + case "entity": + $entityData = '['; + foreach ($stepsData[$customActionAttributes['name']] as $dataKey => $dataValue) { + $variableReplace = $this->resolveTestVariable($dataValue, true); + $entityData .= sprintf("\"%s\" => \"%s\", ", $dataKey, $variableReplace); + } + $entityData .= ']'; + if ($hookObject) { + // no uniqueness attributes for data allowed within entity defined in cest. + $testSteps .= sprintf( + "\t\t\$this->%s = new EntityDataObject(\"%s\",\"%s\",%s,null,null);\n", + $customActionAttributes['name'], + $customActionAttributes['name'], + $customActionAttributes['type'], + $entityData + ); + } else { + // no uniqueness attributes for data allowed within entity defined in cest. + $testSteps .= sprintf( + "\t\t$%s = new EntityDataObject(\"%s\",\"%s\",%s,null,null);\n", + $customActionAttributes['name'], + $customActionAttributes['name'], + $customActionAttributes['type'], + $entityData + ); + } + break; + case "dontSeeCurrentUrlEquals": + case "dontSeeCurrentUrlMatches": + case "seeInPopup": + case "saveSessionSnapshot": + case "seeCurrentUrlEquals": + case "seeCurrentUrlMatches": + case "seeInTitle": + case "seeInCurrentUrl": + case "switchToIFrame": + case "switchToWindow": + case "typeInPopup": + case "dontSee": + case "see": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $input, $selector); + break; + case "switchToNextTab": + case "switchToPreviousTab": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $this->stripWrappedQuotes($input)); + break; + case "clickWithLeftButton": + case "clickWithRightButton": + case "moveMouseOver": + case "scrollTo": + if (!$selector) { + $selector = 'null'; + } + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $x, $y); + break; + case "dontSeeCookie": + case "resetCookie": + case "seeCookie": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $input, $parameterArray); + break; + case "grabCookie": + $testSteps .= $this->wrapFunctionCallWithReturnValue( + $returnVariable, + $actor, + $actionName, + $input, + $parameterArray + ); + break; + case "dontSeeElement": + case "dontSeeElementInDOM": + case "dontSeeInFormFields": + case "seeElement": + case "seeElementInDOM": + case "seeInFormFields": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $parameterArray); + break; + case "pressKey": + case "selectOption": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $input, $parameterArray); + break; + case "submitForm": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $parameterArray, $button); + break; + case "dragAndDrop": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector1, $selector2); + break; + case "executeInSelenium": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $function); + break; + case "executeJS": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $this->wrapWithSingleQuotes($function)); + break; + case "performOn": + case "waitForElementChange": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $function, $time); + break; + case "waitForJS": + $testSteps .= $this->wrapFunctionCall( + $actor, + $actionName, + $this->wrapWithSingleQuotes($function), + $time + ); + break; + case "wait": + case "waitForAjaxLoad": + case "waitForElement": + case "waitForElementVisible": + case "waitForElementNotVisible": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $time); + break; + case "waitForPageLoad": + case "waitForText": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $input, $time, $selector); + break; + case "formatMoney": + case "mSetLocale": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $input, $locale); + break; + case "grabAttributeFrom": + case "grabMultiple": + case "grabFromCurrentUrl": + if (isset($returnVariable)) { + $testSteps .= $this->wrapFunctionCallWithReturnValue( + $returnVariable, + $actor, + $actionName, + $selector, + $input + ); + } else { + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $input); + } + break; + case "grabValueFrom": + if (isset($returnVariable)) { + $testSteps .= $this->wrapFunctionCallWithReturnValue( + $returnVariable, + $actor, + $actionName, + $selector + ); + } else { + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector); + } + break; + case "loginAsAdmin": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $username, $password); + break; + case "resizeWindow": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $width, $height); + break; + case "searchAndMultiSelectOption": + $testSteps .= $this->wrapFunctionCall( + $actor, + $actionName, + $selector, + $input, + $parameterArray, + $requiredAction + ); + break; + case "seeLink": + case "dontSeeLink": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $input, $url); + break; + case "setCookie": + $testSteps .= $this->wrapFunctionCall( + $actor, + $actionName, + $selector, + $input, + $value, + $parameterArray + ); + break; + case "amOnPage": + case "amOnSubdomain": + case "amOnUrl": + case "appendField": + case "attachFile": + case "click": + case "dontSeeInField": + case "dontSeeInCurrentUrl": + case "dontSeeInTitle": + case "dontSeeInPageSource": + case "dontSeeOptionIsSelected": + case "fillField": + case "loadSessionSnapshot": + case "seeInField": + case "seeOptionIsSelected": + case "unselectOption": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $input); + break; + case "seeNumberOfElements": + $testSteps .= $this->wrapFunctionCall( + $actor, + $actionName, + $selector, + $this->stripWrappedQuotes($input), + $parameterArray + ); + break; + case "seeInPageSource": + case "seeInSource": + case "dontSeeInSource": + // TODO: Need to fix xml parser to allow parsing html. + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $html); + break; + default: + if ($returnVariable) { + $testSteps .= $this->wrapFunctionCallWithReturnValue( + $returnVariable, + $actor, + $actionName, + $selector, + $input, + $parameter + ); + } else { + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $input, $parameter); + } + } + } + + return $testSteps; + } + + /** + * Resolves replacement of $input$ and $$input$$ in given string. + * Can be given a boolean to surround replacement with quote breaking. + * @param string $inputString + * @param bool $quoteBreak + * @return string + * @throws \Exception + */ + private function resolveTestVariable($inputString, $quoteBreak = false) + { + $outputString = $inputString; + $replaced = false; + + // Check for Cest-scope variables first, stricter regex match. + preg_match_all("/\\$\\$[\w.]+\\$\\$/", $outputString, $matches); + foreach ($matches[0] as $match) { + $replacement = null; + $variable = $this->stripAndSplitReference($match, '$$'); + if (count($variable) != 2) { + throw new \Exception( + "Invalid Persisted Entity Reference: " . $match . + ". Hook persisted entity references must follow \$\$entityMergeKey.field\$\$ format." + ); + } + $replacement = sprintf("\$this->%s->getCreatedDataByName('%s')", $variable[0], $variable[1]); + if ($quoteBreak) { + $replacement = '" . ' . $replacement . ' . "'; + } + $outputString = str_replace($match, $replacement, $outputString); + $replaced = true; + } + + // Check Test-scope variables + preg_match_all("/\\$[\w.]+\\$/", $outputString, $matches); + foreach ($matches[0] as $match) { + $replacement = null; + $variable = $this->stripAndSplitReference($match, '$'); + if (count($variable) != 2) { + throw new \Exception( + "Invalid Persisted Entity Reference: " . $match . + ". Test persisted entity references must follow \$entityMergeKey.field\$ format." + ); + } + $replacement = sprintf("$%s->getCreatedDataByName('%s')", $variable[0], $variable[1]); + if ($quoteBreak) { + $replacement = '" . ' . $replacement . ' . "'; + } + $outputString = str_replace($match, $replacement, $outputString); + $replaced = true; + } + + return $outputString; + } + + /** + * Performs str_replace on variable reference, dependent on delimiter and returns exploded array. + * @param string $reference + * @param string $delimiter + * @return array + */ + private function stripAndSplitReference($reference, $delimiter) + { + $strippedReference = str_replace($delimiter, '', $reference); + return explode('.', $strippedReference); + } + + /** + * Creates a PHP string for the _before/_after methods if the Test contains an or block. + * @param array $hookObjects + * @return string + */ + private function generateHooksPhp($hookObjects) + { + $hooks = ""; + $createData = false; + foreach ($hookObjects as $hookObject) { + $type = $hookObject->getType(); + $dependencies = 'AcceptanceTester $I'; + + foreach ($hookObject->getActions() as $step) { + if ($step->getType() == "createData") { + $hooks .= "\t/**\n"; + $hooks .= sprintf("\t * @var EntityApiHandler $%s;\n", $step->getMergeKey()); + $hooks .= "\t */\n"; + $hooks .= sprintf("\tprotected $%s;\n\n", $step->getMergeKey()); + $createData = true; + } elseif ($step->getType() == "entity") { + $hooks .= "\t/**\n"; + $hooks .= sprintf("\t * @var EntityDataObject $%s;\n", $step->getMergeKey()); + $hooks .= "\t */\n"; + $hooks .= sprintf("\tprotected $%s;\n\n", $step->getCustomActionAttributes()['name']); + } + } + + $steps = $this->generateStepsPhp($hookObject->getActions(), $hookObject->getCustomData(), $createData); + + if ($type == "after") { + $hooks .= sprintf("\tpublic function _after(%s)\n", $dependencies); + $hooks .= "\t{\n"; + $hooks .= $steps; + $hooks .= "\t}\n\n"; + } + + if ($type == "before") { + $hooks .= sprintf("\tpublic function _before(%s)\n", $dependencies); + $hooks .= "\t{\n"; + $hooks .= $steps; + $hooks .= "\t}\n\n"; + } + + $hooks .= ""; + } + + return $hooks; + } + + /** + * Creates a PHP string for the Test Annotations block if the Test contains an block. + * + * @param array $testAnnotationsObject + * @return string + */ + private function generateTestAnnotationsPhp($testAnnotationsObject) + { + $testAnnotationsPhp = "\t/**\n"; + + foreach ($testAnnotationsObject as $annotationType => $annotationName) { + if ($annotationType == "features") { + $features = ""; + + foreach ($annotationName as $name) { + $features .= sprintf("\"%s\"", $name); + + if (next($annotationName)) { + $features .= ", "; + } + } + + $testAnnotationsPhp .= sprintf("\t * @Features({%s})\n", $features); + } + + if ($annotationType == "stories") { + $stories = ""; + + foreach ($annotationName as $name) { + $stories .= sprintf("\"%s\"", $name); + + if (next($annotationName)) { + $stories .= ", "; + } + } + + $testAnnotationsPhp .= sprintf("\t * @Stories({%s})\n", $stories); + } + + if ($annotationType == "title") { + $testAnnotationsPhp .= sprintf("\t * @Title(\"%s\")\n", $annotationName[0]); + } + + if ($annotationType == "description") { + $testAnnotationsPhp .= sprintf("\t * @Description(\"%s\")\n", $annotationName[0]); + } + + if ($annotationType == "severity") { + $testAnnotationsPhp .= sprintf( + "\t * @Severity(level = SeverityLevel::%s)\n", + $annotationName[0] + ); + } + + if ($annotationType == "testCaseId") { + $testAnnotationsPhp .= sprintf("\t * @TestCaseId(\"%s\")\n", $annotationName[0]); + } + } + + $testAnnotationsPhp .= sprintf( + "\t * @Parameter(name = \"%s\", value=\"$%s\")\n", + "AcceptanceTester", + "I" + ); + + foreach ($testAnnotationsObject as $annotationType => $annotationName) { + if ($annotationType == "group") { + foreach ($annotationName as $name) { + $testAnnotationsPhp .= sprintf("\t * @group %s\n", $name); + } + } + + if ($annotationType == "env") { + foreach ($annotationName as $env) { + $testAnnotationsPhp .= sprintf("\t * @env %s\n", $env); + } + } + } + + $testAnnotationsPhp .= sprintf("\t * @param %s $%s\n", "AcceptanceTester", "I"); + $testAnnotationsPhp .= "\t * @return void\n"; + $testAnnotationsPhp .= "\t */\n"; + + return $testAnnotationsPhp; + } + + /** + * Creates a PHP string based on a block. + * Concatenates the Test Annotations PHP and Test PHP for a single Test. + * @param array $testsObject + * @return string + */ + private function generateTestsPhp($testsObject) + { + $testPhp = ""; + + foreach ($testsObject as $test) { + $testName = $test->getName(); + $testName = str_replace(' ', '', $testName); + $testAnnotations = $this->generateTestAnnotationsPhp($test->getAnnotations()); + $dependencies = 'AcceptanceTester $I'; + $steps = $this->generateStepsPhp($test->getOrderedActions(), $test->getCustomData()); + + $testPhp .= $testAnnotations; + $testPhp .= sprintf("\tpublic function %s(%s)\n", $testName, $dependencies); + $testPhp .= "\t{\n"; + $testPhp .= $steps; + $testPhp .= "\t}\n"; + + if (sizeof($testsObject) > 1) { + $testPhp .= "\n"; + } + } + + return $testPhp; + } + + /** + * Add uniqueness function call to input string based on regex pattern. + * + * @param string $input + * @return string + */ + private function addUniquenessFunctionCall($input) + { + $output = ''; + + preg_match('/' . EntityDataObject::CEST_UNIQUE_FUNCTION .'\("[\w]+"\)/', $input, $matches); + if (!empty($matches)) { + $parts = preg_split('/' . EntityDataObject::CEST_UNIQUE_FUNCTION . '\("[\w]+"\)/', $input, -1); + for ($i = 0; $i < count($parts); $i++) { + $parts[$i] = $this->stripWrappedQuotes($parts[$i]); + } + if (!empty($parts[0])) { + $output = $this->wrapWithSingleQuotes($parts[0]); + } + $output .= $output === '' ? $matches[0] : '.' . $matches[0]; + if (!empty($parts[1])) { + $output .= '.' . $this->wrapWithSingleQuotes($parts[1]); + } + } else { + $output = $this->wrapWithSingleQuotes($input); + } + + return $output; + } + + /** + * Wrap input string with single quotes. + * + * @param string $input + * @return string + */ + private function wrapWithSingleQuotes($input) + { + if (empty($input)) { + return ''; + } + $input = addslashes($input); + return sprintf('"%s"', $input); + } + + /** + * Strip beginning and ending quotes of input string. + * + * @param string $input + * @return string + */ + private function stripWrappedQuotes($input) + { + if (empty($input)) { + return ''; + } + if (substr($input, 0, 1) === '"' || substr($input, 0, 1) === "'") { + $input = substr($input, 1); + } + if (substr($input, -1, 1) === '"' || substr($input, -1, 1) === "'") { + $input = substr($input, 0, -1); + } + return $input; + } + + /** + * Add dollar sign at the beginning of input string. + * + * @param string $input + * @return string + */ + private function addDollarSign($input) + { + return sprintf("$%s", $input); + } + + // @codingStandardsIgnoreStart + /** + * Wrap parameters into a function call. + * + * @param string $actor + * @param string $action + * @param array ...$args + * @return string + */ + private function wrapFunctionCall($actor, $action, ...$args) + { + $isFirst = true; + $output = sprintf("\t\t$%s->%s(", $actor, $action); + for ($i = 0; $i < count($args); $i++) { + if (null === $args[$i]) { + continue; + } + if (!$isFirst) { + $output .= ', '; + } + $output .= $args[$i]; + $isFirst = false; + } + $output .= ");\n"; + + // TODO put in condiional to prevent unncessary quote break (i.e. there are no strings to be appended to + // variable call. + return $this->resolveTestVariable($output, true); + } + + /** + * Wrap parameters into a function call with a return value. + * + * @param string $returnVariable + * @param string $actor + * @param string $action + * @param array ...$args + * @return string + */ + private function wrapFunctionCallWithReturnValue($returnVariable, $actor, $action, ...$args) + { + $isFirst = true; + $output = sprintf("\t\t$%s = $%s->%s(", $returnVariable, $actor, $action); + for ($i = 0; $i < count($args); $i++) { + if (null === $args[$i]) { + continue; + } + if (!$isFirst) { + $output .= ', '; + } + $output .= $args[$i]; + $isFirst = false; + } + $output .= ");\n"; + + // TODO put in condiional to prevent unncessary quote break (i.e. there are no strings to be appended to + // variable call. + return $output = $this->resolveTestVariable($output, true); + } + // @codingStandardsIgnoreEnd +} diff --git a/src/Magento/FunctionalTestingFramework/Util/msq.php b/src/Magento/FunctionalTestingFramework/Util/msq.php new file mode 100644 index 000000000..4ab212580 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/msq.php @@ -0,0 +1,44 @@ +objectManager = $objectManager; + $this->configData = $configData; + } + + /** + * Get parsed xml data. + * @param string $type + * @return array + */ + public function getData($type) + { + return $this->configData->get($type); + } +} diff --git a/src/Magento/FunctionalTestingFramework/XmlParser/ParserInterface.php b/src/Magento/FunctionalTestingFramework/XmlParser/ParserInterface.php new file mode 100644 index 000000000..0b276f8ab --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/XmlParser/ParserInterface.php @@ -0,0 +1,16 @@ +objectManager = $objectManager; + $this->configData = $configData; + } + + /** + * Get parsed xml data. + * + * @param string $type + * @return array + */ + public function getData($type) + { + return $this->configData->get($type); + } +}