diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e19a0e26..77584fda 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,34 +7,36 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['8.0'] - name: PHP ${{ matrix.php-versions }} + php-versions: ['8.1'] + dependency-versions: ['lowest', 'highest'] + name: PHP ${{ matrix.php-versions }} with ${{ matrix.dependency-versions }} versions of Composer dependencies steps: - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} coverage: pcov - tools: phpstan, phpcs, php-cs-fixer:3, phpunit, composer:v2 - #extensions: mbstring, intl + tools: phpstan, phpcs, php-cs-fixer:3 - name: Checkout uses: actions/checkout@v2 - name: Install - run: composer install + uses: "ramsey/composer-install@v2" + with: + dependency-versions: ${{ matrix.dependency-versions }} - name: Run PHP CS - run: phpcs --standard=PSR12 src tests + run: phpcs --standard=PSR12 src tests config - name: Run PHP CS Fixer run: php-cs-fixer fix --diff --dry-run - name: Run PHPStan - run: phpstan analyse src tests + run: phpstan analyse src tests config - name: Test & Generate Code Coverage - run: phpunit + run: ./vendor/bin/phpunit - name: Upload coverage results to Coveralls env: @@ -42,4 +44,4 @@ jobs: run: | composer global require php-coveralls/php-coveralls php-coveralls --coverage_clover=clover.xml -v - if: matrix.php-versions == '7.4' + if: ${{ github.event_name == 'push' && matrix.php-versions == '8.1' && matrix.dependency-versions == 'lowest' }} diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 00759740..5ec2e2b1 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -3,6 +3,7 @@ $finder = PhpCsFixer\Finder::create() ->in(__DIR__.'/src') ->in(__DIR__.'/tests') + ->in(__DIR__.'/config') ; $config = new PhpCsFixer\Config(); diff --git a/CHANGELOG.md b/CHANGELOG.md index e9fa43e1..8b350899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +3.0.0 +----- + +* Only support Symfony ^6.1 +* Only support PHP ^8.1 +* Isolate model variables <> place variables <> transition variables on runtime +* Support transition expression, make it user friendly +* Add custom commands (upload, download, update/assert clipboard) + 2.0.0 ----- diff --git a/README.md b/README.md index 23acd560..c09a76f5 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ This Bundle is a core library for [Sicope Model][sicope-model], a Model-Based Te ## Requirements -* PHP 8.0 -* Symfony 5.3 +* PHP 8.1 +* Symfony 6.1 * See also the `require` section of [composer.json](composer.json) ## Installation @@ -24,8 +24,9 @@ vendor/bin/phpunit ## Validate code with coding standards ```shell -phpcs --standard=PSR12 src tests -phpstan analyse src tests +phpcs --standard=PSR12 src tests config +php-cs-fixer fix --diff --dry-run +phpstan analyse src tests config ``` ## Built With @@ -65,7 +66,7 @@ This package is available under the [MIT license](LICENSE). [version-image]: https://img.shields.io/github/v/release/tienvx/mbt-bundle?include_prereleases [php-version-url]: https://packagist.org/packages/tienvx/mbt-bundle -[php-version-image]: http://img.shields.io/badge/php-8.0.0+-ff69b4.svg +[php-version-image]: http://img.shields.io/badge/php-8.1.0+-ff69b4.svg [contributors]: https://github.com/tienvx/mbt-bundle/graphs/contributors [pulls]: https://github.com/tienvx/mbt-bundle/pulls diff --git a/composer.json b/composer.json index cb39873f..9d0fb9ee 100644 --- a/composer.json +++ b/composer.json @@ -16,20 +16,21 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ext-json": "*", - "doctrine/annotations": "^1.13", + "doctrine/doctrine-bundle": "^2.6", "doctrine/orm": "^2.8", - "florianv/petrinet": "^2.1", "jmgq/a-star": "^2.1", "php-webdriver/webdriver": "^1.11", - "symfony/config": "^5.4", - "symfony/dependency-injection": "^5.4", - "symfony/expression-language": "^5.4", - "symfony/http-kernel": "^5.4", - "symfony/messenger": "^5.4", - "symfony/validator": "^5.4", - "tienvx/single-color-petrinet": "^1.3" + "symfony/config": "^6.1", + "symfony/dependency-injection": "^6.1", + "symfony/expression-language": "^6.1", + "symfony/http-client": "^6.1", + "symfony/http-kernel": "^6.1", + "symfony/messenger": "^6.1", + "symfony/validator": "^6.1", + "tienvx/single-color-petrinet": "^1.5", + "tienvx/assignments-evaluator-bundle": "^1.0.1" }, "require-dev": { "phpunit/phpunit": "^9.5" diff --git a/config/definition.php b/config/definition.php new file mode 100644 index 00000000..1653d03b --- /dev/null +++ b/config/definition.php @@ -0,0 +1,22 @@ +rootNode() + ->children() + ->scalarNode(TienvxMbtBundle::WEBDRIVER_URI) + ->isRequired() + ->cannotBeEmpty() + ->defaultValue('http://localhost:4444') + ->end() + ->end() + ->children() + ->scalarNode(TienvxMbtBundle::UPLOAD_DIR) + ->isRequired() + ->cannotBeEmpty() + ->end() + ->end() + ; +}; diff --git a/src/Resources/config/services.php b/config/services.php similarity index 70% rename from src/Resources/config/services.php rename to config/services.php index 431636d0..995b92f5 100644 --- a/src/Resources/config/services.php +++ b/config/services.php @@ -2,32 +2,24 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use Petrinet\Builder\MarkingBuilder; use SingleColorPetrinet\Model\ColorfulFactory; use SingleColorPetrinet\Model\ColorfulFactoryInterface; use SingleColorPetrinet\Service\GuardedTransitionService; use SingleColorPetrinet\Service\GuardedTransitionServiceInterface; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Tienvx\AssignmentsEvaluator\AssignmentsEvaluator; use Tienvx\Bundle\MbtBundle\Channel\ChannelManager; use Tienvx\Bundle\MbtBundle\Channel\ChannelManagerInterface; -use Tienvx\Bundle\MbtBundle\Command\CommandPreprocessor; -use Tienvx\Bundle\MbtBundle\Command\CommandPreprocessorInterface; -use Tienvx\Bundle\MbtBundle\Command\CommandRunnerInterface; -use Tienvx\Bundle\MbtBundle\Command\CommandRunnerManager; -use Tienvx\Bundle\MbtBundle\Command\CommandRunnerManagerInterface; -use Tienvx\Bundle\MbtBundle\Command\Runner\AlertCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\AssertionRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\KeyboardCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\MouseCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\ScriptCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\StoreCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\WaitCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\WindowCommandRunner; +use Tienvx\Bundle\MbtBundle\Command\CommandManager; +use Tienvx\Bundle\MbtBundle\Command\CommandManagerInterface; use Tienvx\Bundle\MbtBundle\EventListener\EntitySubscriber; use Tienvx\Bundle\MbtBundle\Generator\GeneratorManager; use Tienvx\Bundle\MbtBundle\Generator\GeneratorManagerInterface; use Tienvx\Bundle\MbtBundle\Generator\RandomGenerator; +use Tienvx\Bundle\MbtBundle\MessageHandler\CreateBugMessageHandler; use Tienvx\Bundle\MbtBundle\MessageHandler\RecordVideoMessageHandler; use Tienvx\Bundle\MbtBundle\MessageHandler\ReduceBugMessageHandler; use Tienvx\Bundle\MbtBundle\MessageHandler\ReduceStepsMessageHandler; @@ -41,13 +33,13 @@ use Tienvx\Bundle\MbtBundle\Reducer\Split\SplitDispatcher; use Tienvx\Bundle\MbtBundle\Reducer\Split\SplitHandler; use Tienvx\Bundle\MbtBundle\Reducer\Split\SplitReducer; -use Tienvx\Bundle\MbtBundle\Service\AStar\PetrinetDomainLogic; -use Tienvx\Bundle\MbtBundle\Service\AStar\PetrinetDomainLogicInterface; +use Tienvx\Bundle\MbtBundle\Repository\BugRepository; +use Tienvx\Bundle\MbtBundle\Repository\BugRepositoryInterface; +use Tienvx\Bundle\MbtBundle\Repository\TaskRepository; +use Tienvx\Bundle\MbtBundle\Repository\TaskRepositoryInterface; use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelper; use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelperInterface; use Tienvx\Bundle\MbtBundle\Service\Bug\BugNotifierInterface; -use Tienvx\Bundle\MbtBundle\Service\Bug\BugProgress; -use Tienvx\Bundle\MbtBundle\Service\Bug\BugProgressInterface; use Tienvx\Bundle\MbtBundle\Service\ConfigInterface; use Tienvx\Bundle\MbtBundle\Service\ExpressionLanguage; use Tienvx\Bundle\MbtBundle\Service\Model\ModelDumper; @@ -60,10 +52,12 @@ use Tienvx\Bundle\MbtBundle\Service\Petrinet\PetrinetHelperInterface; use Tienvx\Bundle\MbtBundle\Service\SelenoidHelper; use Tienvx\Bundle\MbtBundle\Service\SelenoidHelperInterface; -use Tienvx\Bundle\MbtBundle\Service\ShortestPathStepsBuilder; -use Tienvx\Bundle\MbtBundle\Service\StepRunner; -use Tienvx\Bundle\MbtBundle\Service\StepRunnerInterface; -use Tienvx\Bundle\MbtBundle\Service\StepsBuilderInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Builder\ShortestPathStepsBuilder; +use Tienvx\Bundle\MbtBundle\Service\Step\Builder\StepsBuilderInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Runner\BugStepsRunner; +use Tienvx\Bundle\MbtBundle\Service\Step\Runner\ExploreStepsRunner; +use Tienvx\Bundle\MbtBundle\Service\Step\Runner\StepRunner; +use Tienvx\Bundle\MbtBundle\Service\Step\Runner\StepRunnerInterface; use Tienvx\Bundle\MbtBundle\Service\Task\TaskHelper; use Tienvx\Bundle\MbtBundle\Service\Task\TaskHelperInterface; use Tienvx\Bundle\MbtBundle\Validator\TagsValidator; @@ -92,51 +86,53 @@ ]) ->autoconfigure(true) + // Message handlers ->set(RunTaskMessageHandler::class) ->args([ service(TaskHelperInterface::class), ]) ->autoconfigure(true) - ->set(RecordVideoMessageHandler::class) ->args([ service(BugHelperInterface::class), ]) ->autoconfigure(true) - ->set(ReduceBugMessageHandler::class) ->args([ service(BugHelperInterface::class), ]) ->autoconfigure(true) - ->set(ReduceStepsMessageHandler::class) ->args([ service(BugHelperInterface::class), ]) ->autoconfigure(true) - ->set(ReportBugMessageHandler::class) ->args([ service(BugHelperInterface::class), ]) ->autoconfigure(true) + ->set(CreateBugMessageHandler::class) + ->args([ + service(ConfigInterface::class), + service(TaskRepositoryInterface::class), + ]) + ->autoconfigure(true) + // Reducers ->set(ReducerManager::class) ->alias(ReducerManagerInterface::class, ReducerManager::class) - ->set(RandomDispatcher::class) ->args([ service(MessageBusInterface::class), ]) ->set(RandomHandler::class) ->args([ - service(EntityManagerInterface::class), + service(BugRepositoryInterface::class), service(MessageBusInterface::class), - service(StepRunnerInterface::class), + service(BugStepsRunner::class), service(StepsBuilderInterface::class), - service(BugHelperInterface::class), - service(SelenoidHelperInterface::class), + service(ConfigInterface::class), ]) ->set(RandomReducer::class) ->args([ @@ -150,12 +146,11 @@ ]) ->set(SplitHandler::class) ->args([ - service(EntityManagerInterface::class), + service(BugRepositoryInterface::class), service(MessageBusInterface::class), - service(StepRunnerInterface::class), + service(BugStepsRunner::class), service(StepsBuilderInterface::class), - service(BugHelperInterface::class), - service(SelenoidHelperInterface::class), + service(ConfigInterface::class), ]) ->set(SplitReducer::class) ->args([ @@ -164,44 +159,40 @@ ]) ->autoconfigure(true) + // Validators ->set(TagsValidator::class) ->set(ValidCommandValidator::class) ->args([ - service(CommandRunnerManagerInterface::class), + service(CommandManagerInterface::class), ]) ->tag('validator.constraint_validator', [ 'alias' => ValidCommandValidator::class, ]) - ->set(CommandPreprocessor::class) - ->alias(CommandPreprocessorInterface::class, CommandPreprocessor::class) - - ->set(CommandRunnerManager::class) + // Commands + ->set(CommandManager::class) ->args([ - tagged_iterator(CommandRunnerInterface::TAG), - service(CommandPreprocessorInterface::class), + service(HttpClientInterface::class), ]) - ->alias(CommandRunnerManagerInterface::class, CommandRunnerManager::class) + ->alias(CommandManagerInterface::class, CommandManager::class) - ->set(AlertCommandRunner::class) - ->autoconfigure(true) - ->set(AssertionRunner::class) - ->autoconfigure(true) - ->set(KeyboardCommandRunner::class) - ->autoconfigure(true) - ->set(MouseCommandRunner::class) - ->autoconfigure(true) - ->set(ScriptCommandRunner::class) - ->autoconfigure(true) - ->set(StoreCommandRunner::class) - ->autoconfigure(true) - ->set(WaitCommandRunner::class) - ->autoconfigure(true) - ->set(WindowCommandRunner::class) - ->autoconfigure(true) + // Repositories + ->set(BugRepository::class) + ->args([ + service(ManagerRegistry::class), + ]) + ->tag('doctrine.repository_service') + ->alias(BugRepositoryInterface::class, BugRepository::class) + ->set(TaskRepository::class) + ->args([ + service(ManagerRegistry::class), + ]) + ->tag('doctrine.repository_service') + ->alias(TaskRepositoryInterface::class, TaskRepository::class) // Services ->set(ExpressionLanguage::class) + ->set('assignments_evaluator.expression_language', ExpressionLanguage::class) ->set(SelenoidHelper::class) ->alias(SelenoidHelperInterface::class, SelenoidHelper::class) @@ -212,32 +203,23 @@ ->set(ModelHelper::class) ->alias(ModelHelperInterface::class, ModelHelper::class) - ->set(BugProgress::class) - ->args([ - service(EntityManagerInterface::class), - ]) - ->alias(BugProgressInterface::class, BugProgress::class) - ->set(BugHelper::class) ->args([ - service(ReducerManager::class), - service(EntityManagerInterface::class), + service(ReducerManagerInterface::class), + service(BugRepositoryInterface::class), service(MessageBusInterface::class), - service(BugProgressInterface::class), service(BugNotifierInterface::class), - service(StepRunnerInterface::class), - service(SelenoidHelperInterface::class), + service(BugStepsRunner::class), service(ConfigInterface::class), ]) ->alias(BugHelperInterface::class, BugHelper::class) ->set(TaskHelper::class) ->args([ - service(GeneratorManager::class), - service(EntityManagerInterface::class), - service(StepRunnerInterface::class), - service(BugHelperInterface::class), - service(SelenoidHelperInterface::class), + service(GeneratorManagerInterface::class), + service(TaskRepositoryInterface::class), + service(MessageBusInterface::class), + service(ExploreStepsRunner::class), service(ConfigInterface::class), ]) ->alias(TaskHelperInterface::class, TaskHelper::class) @@ -245,23 +227,29 @@ ->set(ShortestPathStepsBuilder::class) ->args([ service(PetrinetHelperInterface::class), - service(PetrinetDomainLogicInterface::class), - ]) - ->alias(StepsBuilderInterface::class, ShortestPathStepsBuilder::class) - - ->set(PetrinetDomainLogic::class) - ->args([ service(GuardedTransitionServiceInterface::class), service(MarkingHelperInterface::class), ]) - ->alias(PetrinetDomainLogicInterface::class, PetrinetDomainLogic::class) + ->alias(StepsBuilderInterface::class, ShortestPathStepsBuilder::class) ->set(StepRunner::class) ->args([ - service(CommandRunnerManagerInterface::class), + service(CommandManagerInterface::class), ]) ->alias(StepRunnerInterface::class, StepRunner::class) + ->set(ExploreStepsRunner::class) + ->args([ + service(SelenoidHelperInterface::class), + service(StepRunnerInterface::class), + service(ConfigInterface::class), + ]) + ->set(BugStepsRunner::class) + ->args([ + service(SelenoidHelperInterface::class), + service(StepRunnerInterface::class), + ]) + ->set(MarkingHelper::class) ->args([ service(ColorfulFactoryInterface::class), @@ -272,6 +260,7 @@ ->args([ service(ColorfulFactoryInterface::class), service(ExpressionLanguage::class), + service(AssignmentsEvaluator::class), ]) ->alias(PetrinetHelperInterface::class, PetrinetHelper::class) diff --git a/src/Channel/ChannelManager.php b/src/Channel/ChannelManager.php index 86d88741..841c8cbd 100644 --- a/src/Channel/ChannelManager.php +++ b/src/Channel/ChannelManager.php @@ -2,17 +2,22 @@ namespace Tienvx\Bundle\MbtBundle\Channel; -use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; -use Tienvx\Bundle\MbtBundle\Plugin\AbstractPluginManager; +use Tienvx\Bundle\MbtBundle\Plugin\PluginManager; -class ChannelManager extends AbstractPluginManager implements ChannelManagerInterface +class ChannelManager extends PluginManager implements ChannelManagerInterface { public function getChannel(string $name): ChannelInterface { - if ($this->has($name) && ($channel = $this->get($name)) && $channel instanceof ChannelInterface) { - return $channel; - } + return parent::get($name); + } - throw new UnexpectedValueException(sprintf('Channel "%s" does not exist.', $name)); + protected function getPluginInterface(): string + { + return ChannelInterface::class; + } + + protected function getInvalidPluginExceptionMessage(string $name): string + { + return sprintf('Channel "%s" does not exist.', $name); } } diff --git a/src/Command/CommandRunner.php b/src/Command/AbstractCommand.php similarity index 54% rename from src/Command/CommandRunner.php rename to src/Command/AbstractCommand.php index 1ce9b730..128b82a6 100644 --- a/src/Command/CommandRunner.php +++ b/src/Command/AbstractCommand.php @@ -2,28 +2,53 @@ namespace Tienvx\Bundle\MbtBundle\Command; -use Facebook\WebDriver\Exception\UnexpectedTagNameException; +use Exception; use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverElement; use Facebook\WebDriver\WebDriverSelect; use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; -use Tienvx\Bundle\MbtBundle\Model\Model\Revision\CommandInterface; +use Tienvx\Bundle\MbtBundle\Model\ValuesInterface; -abstract class CommandRunner implements CommandRunnerInterface +abstract class AbstractCommand implements CommandInterface { - public function supports(CommandInterface $command): bool + public static function getValueHelper(): string { - return in_array($command->getCommand(), $this->getAllCommands()); + return ''; } - protected function isValidSelector(string $target): bool + public static function getTargetHelper(): string + { + return "Locator e.g. 'id=email' or 'css=.last-name'"; + } + + public static function validateTarget(?string $target): bool + { + return $target && static::isValidSelector($target); + } + + protected static function isValidSelector(string $target): bool { list($mechanism) = explode('=', $target, 2); return in_array($mechanism, static::MECHANISMS); } + public function validateValue(?string $value): bool + { + return !static::isValueRequired() || !is_null($value); + } + + public function run(?string $target, ?string $value, ValuesInterface $values, RemoteWebDriver $driver): void + { + if (static::isTargetRequired() && empty($target)) { + throw new UnexpectedValueException('Target is required'); + } + if (static::isValueRequired() && empty($value)) { + throw new UnexpectedValueException('Value is required'); + } + } + protected function getSelector(string $target): WebDriverBy { list($mechanism, $value) = explode('=', $target, 2); @@ -33,7 +58,7 @@ protected function getSelector(string $target): WebDriverBy case static::MECHANISM_LINK_TEXT: case static::MECHANISM_PARTIAL_LINK_TEXT: case static::MECHANISM_XPATH: - return WebDriverBy::{$mechanism}($value); + return WebDriverBy::$mechanism($value); case static::MECHANISM_CSS: return WebDriverBy::cssSelector($value); default: @@ -58,4 +83,11 @@ protected function getSelect(WebDriverElement $element): WebDriverSelect { return new WebDriverSelect($element); } + + protected function assert(bool $assertion, string $message): void + { + if (!$assertion) { + throw new Exception($message); + } + } } diff --git a/src/Command/Alert/AbstractAlertCommand.php b/src/Command/Alert/AbstractAlertCommand.php new file mode 100644 index 00000000..99312aff --- /dev/null +++ b/src/Command/Alert/AbstractAlertCommand.php @@ -0,0 +1,23 @@ +switchTo()->alert()->accept(); + } +} diff --git a/src/Command/Alert/AnswerPromptCommand.php b/src/Command/Alert/AnswerPromptCommand.php new file mode 100644 index 00000000..b5880416 --- /dev/null +++ b/src/Command/Alert/AnswerPromptCommand.php @@ -0,0 +1,37 @@ +switchTo()->alert(); + $alert->sendKeys($target); + $alert->accept(); + } +} diff --git a/src/Command/Alert/DismissPromptCommand.php b/src/Command/Alert/DismissPromptCommand.php new file mode 100644 index 00000000..6c88c67e --- /dev/null +++ b/src/Command/Alert/DismissPromptCommand.php @@ -0,0 +1,25 @@ +switchTo()->alert()->dismiss(); + } +} diff --git a/src/Command/Assert/AbstractAssertCommand.php b/src/Command/Assert/AbstractAssertCommand.php new file mode 100644 index 00000000..ea3ee97f --- /dev/null +++ b/src/Command/Assert/AbstractAssertCommand.php @@ -0,0 +1,13 @@ +switchTo()->alert()->getText(); + $this->assert( + $alertText === $target, + sprintf('Actual alert text "%s" did not match "%s"', $alertText, $target) + ); + } +} diff --git a/src/Command/Assert/AssertCheckedCommand.php b/src/Command/Assert/AssertCheckedCommand.php new file mode 100644 index 00000000..5ce9bd88 --- /dev/null +++ b/src/Command/Assert/AssertCheckedCommand.php @@ -0,0 +1,28 @@ +assert( + $driver->findElement($this->getSelector($target))->isSelected(), + sprintf('Element "%s" is not checked, expected to be checked', $target) + ); + } +} diff --git a/src/Command/Assert/AssertCommand.php b/src/Command/Assert/AssertCommand.php new file mode 100644 index 00000000..f229187b --- /dev/null +++ b/src/Command/Assert/AssertCommand.php @@ -0,0 +1,44 @@ +getValue($target); + $this->assert( + $actual === $value, + sprintf('Actual value "%s" did not match "%s"', $actual, $value) + ); + } +} diff --git a/src/Command/Assert/AssertEditableCommand.php b/src/Command/Assert/AssertEditableCommand.php new file mode 100644 index 00000000..e6b20c81 --- /dev/null +++ b/src/Command/Assert/AssertEditableCommand.php @@ -0,0 +1,28 @@ +assert( + $this->isElementEditable($driver, $driver->findElement($this->getSelector($target))), + sprintf('Element "%s" is not editable', $target) + ); + } +} diff --git a/src/Command/Assert/AssertElementNotPresentCommand.php b/src/Command/Assert/AssertElementNotPresentCommand.php new file mode 100644 index 00000000..9150a468 --- /dev/null +++ b/src/Command/Assert/AssertElementNotPresentCommand.php @@ -0,0 +1,28 @@ +assert( + 0 === count($driver->findElements($this->getSelector($target))), + sprintf('Unexpected element "%s" was found in page', $target) + ); + } +} diff --git a/src/Command/Assert/AssertElementPresentCommand.php b/src/Command/Assert/AssertElementPresentCommand.php new file mode 100644 index 00000000..96a791a6 --- /dev/null +++ b/src/Command/Assert/AssertElementPresentCommand.php @@ -0,0 +1,28 @@ +assert( + count($driver->findElements($this->getSelector($target))) > 0, + sprintf('Expected element "%s" was not found in page', $target) + ); + } +} diff --git a/src/Command/Assert/AssertNotCheckedCommand.php b/src/Command/Assert/AssertNotCheckedCommand.php new file mode 100644 index 00000000..4a5e66a6 --- /dev/null +++ b/src/Command/Assert/AssertNotCheckedCommand.php @@ -0,0 +1,28 @@ +assert( + !$driver->findElement($this->getSelector($target))->isSelected(), + sprintf('Element "%s" is checked, expected to be unchecked', $target) + ); + } +} diff --git a/src/Command/Assert/AssertNotEditableCommand.php b/src/Command/Assert/AssertNotEditableCommand.php new file mode 100644 index 00000000..48c9b745 --- /dev/null +++ b/src/Command/Assert/AssertNotEditableCommand.php @@ -0,0 +1,28 @@ +assert( + !$this->isElementEditable($driver, $driver->findElement($this->getSelector($target))), + sprintf('Element "%s" is editable', $target) + ); + } +} diff --git a/src/Command/Assert/AssertNotSelectedLabelCommand.php b/src/Command/Assert/AssertNotSelectedLabelCommand.php new file mode 100644 index 00000000..8b4b6ce6 --- /dev/null +++ b/src/Command/Assert/AssertNotSelectedLabelCommand.php @@ -0,0 +1,35 @@ +getSelect($driver->findElement($this->getSelector($target))); + $elementLabel = $select->getFirstSelectedOption()->getText(); + $this->assert( + $elementLabel !== $value, + sprintf('Actual label "%s" did match "%s"', $elementLabel, $value) + ); + } +} diff --git a/src/Command/Assert/AssertNotSelectedValueCommand.php b/src/Command/Assert/AssertNotSelectedValueCommand.php new file mode 100644 index 00000000..edd54a8c --- /dev/null +++ b/src/Command/Assert/AssertNotSelectedValueCommand.php @@ -0,0 +1,35 @@ +getSelect($driver->findElement($this->getSelector($target))); + $elementValue = $select->getFirstSelectedOption()->getAttribute('value'); + $this->assert( + $elementValue !== $value, + sprintf('Actual value "%s" did match "%s"', $elementValue, $value) + ); + } +} diff --git a/src/Command/Assert/AssertNotTextCommand.php b/src/Command/Assert/AssertNotTextCommand.php new file mode 100644 index 00000000..0c723316 --- /dev/null +++ b/src/Command/Assert/AssertNotTextCommand.php @@ -0,0 +1,34 @@ +findElement($this->getSelector($target))->getText(); + $this->assert( + $elementText !== $value, + sprintf('Actual text "%s" did match "%s"', $elementText, $value) + ); + } +} diff --git a/src/Command/Assert/AssertSelectedLabelCommand.php b/src/Command/Assert/AssertSelectedLabelCommand.php new file mode 100644 index 00000000..820368af --- /dev/null +++ b/src/Command/Assert/AssertSelectedLabelCommand.php @@ -0,0 +1,35 @@ +getSelect($driver->findElement($this->getSelector($target))); + $elementLabel = $select->getFirstSelectedOption()->getText(); + $this->assert( + $elementLabel === $value, + sprintf('Actual label "%s" did not match "%s"', $elementLabel, $value) + ); + } +} diff --git a/src/Command/Assert/AssertSelectedValueCommand.php b/src/Command/Assert/AssertSelectedValueCommand.php new file mode 100644 index 00000000..94f64cb7 --- /dev/null +++ b/src/Command/Assert/AssertSelectedValueCommand.php @@ -0,0 +1,35 @@ +getSelect($driver->findElement($this->getSelector($target))); + $elementValue = $select->getFirstSelectedOption()->getAttribute('value'); + $this->assert( + $elementValue === $value, + sprintf('Actual value "%s" did not match "%s"', $elementValue, $value) + ); + } +} diff --git a/src/Command/Assert/AssertTextCommand.php b/src/Command/Assert/AssertTextCommand.php new file mode 100644 index 00000000..a2a600c2 --- /dev/null +++ b/src/Command/Assert/AssertTextCommand.php @@ -0,0 +1,34 @@ +findElement($this->getSelector($target))->getText(); + $this->assert( + $elementText === $value, + sprintf('Actual text "%s" did not match "%s"', $elementText, $value) + ); + } +} diff --git a/src/Command/Assert/AssertTitleCommand.php b/src/Command/Assert/AssertTitleCommand.php new file mode 100644 index 00000000..324f1712 --- /dev/null +++ b/src/Command/Assert/AssertTitleCommand.php @@ -0,0 +1,38 @@ +assert( + $driver->getTitle() === $target, + sprintf('Actual title "%s" did not match "%s"', $driver->getTitle(), $target) + ); + } +} diff --git a/src/Command/Assert/AssertValueCommand.php b/src/Command/Assert/AssertValueCommand.php new file mode 100644 index 00000000..1077d1f2 --- /dev/null +++ b/src/Command/Assert/AssertValueCommand.php @@ -0,0 +1,34 @@ +findElement($this->getSelector($target))->getAttribute('value'); + $this->assert( + $elementValue === $value, + sprintf('Actual value "%s" did not match "%s"', $elementValue, $value) + ); + } +} diff --git a/src/Command/CommandInterface.php b/src/Command/CommandInterface.php new file mode 100644 index 00000000..32203505 --- /dev/null +++ b/src/Command/CommandInterface.php @@ -0,0 +1,38 @@ +uploadDir = $uploadDir; + } + + public function hasCommand(string $command): bool + { + return isset(static::COMMANDS[$command]); + } + + public function isTargetMissing(string $command, ?string $target): bool + { + return $this->hasCommand($command) && + call_user_func([static::COMMANDS[$command], 'isTargetRequired']) && + empty($target); + } + + public function isTargetNotValid(string $command, ?string $target): bool + { + return $this->hasCommand($command) && + !call_user_func([static::COMMANDS[$command], 'validateTarget'], $target); + } + + public function isValueMissing(string $command, ?string $value): bool + { + return $this->hasCommand($command) && + call_user_func([static::COMMANDS[$command], 'isValueRequired']) && + empty($value); + } + + public function isValueNotValid(string $command, ?string $value): bool + { + return $this->hasCommand($command) && + !call_user_func([$this->createCommand($command), 'validateValue'], $value); + } + + public function run( + string $command, + ?string $target, + ?string $value, + ValuesInterface $values, + RemoteWebDriver $driver + ): void { + $this->createCommand($command)->run( + $this->process($target, $values), + $this->process($value, $values), + $values, + $driver + ); + } + + protected function createCommand(string $command): CommandInterface + { + switch ($command) { + case 'upload': + return new UploadCommand($this->uploadDir); + case 'assertFileDownloaded': + return new AssertFileDownloadedCommand($this->httpClient); + case 'assertClipboard': + return new AssertClipboardCommand($this->httpClient); + case 'updateClipboard': + return new UpdateClipboardCommand($this->httpClient); + default: + $className = static::COMMANDS[$command] ?? null; + if ($className) { + return new $className(); + } + + throw new OutOfRangeException(sprintf('Command %s not found', $command)); + } + } + + protected function process(?string $text, ValuesInterface $values): ?string + { + return is_string($text) ? preg_replace_callback( + '/\$\{(.*?)\}/', + fn (array $matches): string => $values->getValues()[$matches[1]] ?? $matches[1], + $text + ) : null; + } +} diff --git a/src/Command/CommandManagerInterface.php b/src/Command/CommandManagerInterface.php new file mode 100644 index 00000000..59f9875c --- /dev/null +++ b/src/Command/CommandManagerInterface.php @@ -0,0 +1,162 @@ + AcceptAlertCommand::class, + 'acceptConfirmation' => AcceptAlertCommand::class, + 'answerPrompt' => AnswerPromptCommand::class, + 'dismissConfirmation' => DismissPromptCommand::class, + 'dismissPrompt' => DismissPromptCommand::class, + 'assert' => AssertCommand::class, + 'assertAlert' => AssertAlertCommand::class, + 'assertConfirmation' => AssertAlertCommand::class, + 'assertPrompt' => AssertAlertCommand::class, + 'assertTitle' => AssertTitleCommand::class, + 'assertText' => AssertTextCommand::class, + 'assertNotText' => AssertNotTextCommand::class, + 'assertValue' => AssertValueCommand::class, + 'assertEditable' => AssertEditableCommand::class, + 'assertNotEditable' => AssertNotEditableCommand::class, + 'assertElementPresent' => AssertElementPresentCommand::class, + 'assertElementNotPresent' => AssertElementNotPresentCommand::class, + 'assertChecked' => AssertCheckedCommand::class, + 'assertNotChecked' => AssertNotCheckedCommand::class, + 'assertSelectedValue' => AssertSelectedValueCommand::class, + 'assertNotSelectedValue' => AssertNotSelectedValueCommand::class, + 'assertSelectedLabel' => AssertSelectedLabelCommand::class, + 'assertNotSelectedLabel' => AssertNotSelectedLabelCommand::class, + 'upload' => UploadCommand::class, + 'assertFileDownloaded' => AssertFileDownloadedCommand::class, + 'assertClipboard' => AssertClipboardCommand::class, + 'updateClipboard' => UpdateClipboardCommand::class, + 'type' => TypeCommand::class, + 'sendKeys' => SendKeysCommand::class, + 'addSelection' => AddSelectionCommand::class, + 'removeSelection' => RemoveSelectionCommand::class, + 'check' => CheckCommand::class, + 'uncheck' => UncheckCommand::class, + 'click' => ClickCommand::class, + 'clickAt' => ClickAtCommand::class, + 'doubleClick' => DoubleClickCommand::class, + 'doubleClickAt' => DoubleClickAtCommand::class, + 'dragAndDropToObject' => DragAndDropToObjectCommand::class, + 'mouseDown' => MouseDownCommand::class, + 'mouseDownAt' => MouseDownAtCommand::class, + 'mouseMoveAt' => MouseMoveAtCommand::class, + 'mouseOut' => MouseOutCommand::class, + 'mouseOver' => MouseOverCommand::class, + 'mouseUp' => MouseUpCommand::class, + 'mouseUpAt' => MouseUpAtCommand::class, + 'select' => SelectCommand::class, + 'runScript' => RunScriptCommand::class, + 'executeScript' => ExecuteScriptCommand::class, + 'executeAsyncScript' => ExecuteAsyncScriptCommand::class, + 'store' => StoreCommand::class, + 'storeAttribute' => StoreAttributeCommand::class, + 'storeElementCount' => StoreElementCountCommand::class, + 'storeJson' => StoreJsonCommand::class, + 'storeText' => StoreTextCommand::class, + 'storeTitle' => StoreTitleCommand::class, + 'storeValue' => StoreValueCommand::class, + 'storeWindowHandle' => StoreWindowHandleCommand::class, + 'waitForElementEditable' => WaitForElementEditableCommand::class, + 'waitForElementNotEditable' => WaitForElementNotEditableCommand::class, + 'waitForElementPresent' => WaitForElementPresentCommand::class, + 'waitForElementNotPresent' => WaitForElementNotPresentCommand::class, + 'waitForElementVisible' => WaitForElementVisibleCommand::class, + 'waitForElementNotVisible' => WaitForElementNotVisibleCommand::class, + 'open' => OpenCommand::class, + 'setWindowSize' => SetWindowSizeCommand::class, + 'selectWindow' => SelectWindowCommand::class, + 'close' => CloseCommand::class, + 'selectFrame' => SelectFrameCommand::class, + ]; + + public function hasCommand(string $command): bool; + + public function isTargetMissing(string $command, ?string $target): bool; + + public function isTargetNotValid(string $command, ?string $target): bool; + + public function isValueMissing(string $command, ?string $value): bool; + + public function isValueNotValid(string $command, ?string $value): bool; + + public function run( + string $command, + ?string $target, + ?string $value, + ValuesInterface $values, + RemoteWebDriver $driver + ): void; +} diff --git a/src/Command/CommandPreprocessor.php b/src/Command/CommandPreprocessor.php deleted file mode 100644 index 6a657468..00000000 --- a/src/Command/CommandPreprocessor.php +++ /dev/null @@ -1,35 +0,0 @@ -setCommand($command->getCommand()); - $processed->setTarget( - $command->getTarget() - ? $this->replaceVariables($command->getTarget(), $color->getValues()) - : $command->getTarget() - ); - $processed->setValue( - $command->getValue() - ? $this->replaceVariables($command->getValue(), $color->getValues()) - : $command->getValue() - ); - - return $processed; - } - - protected function replaceVariables(string $text, array $values): string - { - return preg_replace_callback('/\$\{(.*?)\}/', function ($matches) use ($values) { - return $values[$matches[1]] ?? $matches[1]; - }, $text); - } -} diff --git a/src/Command/CommandPreprocessorInterface.php b/src/Command/CommandPreprocessorInterface.php deleted file mode 100644 index 54a367d5..00000000 --- a/src/Command/CommandPreprocessorInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -runners = $runners; - $this->commandPreprocessor = $commandPreprocessor; - } - - public function getAllCommands(): array - { - return $this->getCommands(); - } - - public function getCommandsRequireTarget(): array - { - return $this->getCommands('getCommandsRequireTarget'); - } - - public function getCommandsRequireValue(): array - { - return $this->getCommands('getCommandsRequireValue'); - } - - public function validateTarget(CommandInterface $command): bool - { - foreach ($this->runners as $runner) { - if ($runner instanceof CommandRunnerInterface && $runner->supports($command)) { - return $runner->validateTarget($command); - } - } - - return false; - } - - public function run(CommandInterface $command, ColorInterface $color, RemoteWebDriver $driver): void - { - foreach ($this->runners as $runner) { - if ($runner instanceof CommandRunnerInterface && $runner->supports($command)) { - $runner->run($this->commandPreprocessor->process($command, $color), $color, $driver); - break; - } - } - } - - protected function getCommands(string $method = 'getAllCommands'): array - { - $commands = []; - foreach ($this->runners as $runner) { - if ($runner instanceof CommandRunnerInterface) { - $commands = array_merge($commands, call_user_func([$runner, $method])); - } - } - - return $commands; - } -} diff --git a/src/Command/CommandRunnerManagerInterface.php b/src/Command/CommandRunnerManagerInterface.php deleted file mode 100644 index f4db5a6e..00000000 --- a/src/Command/CommandRunnerManagerInterface.php +++ /dev/null @@ -1,20 +0,0 @@ -getCommandExecutor()->getAddressOfRemoteServer(), '/wd/hub'), + $type, + $driver->getSessionID() + ); + } +} diff --git a/src/Command/Custom/AssertClipboardCommand.php b/src/Command/Custom/AssertClipboardCommand.php new file mode 100644 index 00000000..bab366ea --- /dev/null +++ b/src/Command/Custom/AssertClipboardCommand.php @@ -0,0 +1,42 @@ +httpClient->request( + 'GET', + $this->getUrl('clipboard', $driver) + )->getContent(); + $this->assert($clipboard === $target, sprintf( + "Failed expecting that clipboard's content equals '%s', actual value '%s'", + $target, + $clipboard + )); + } catch (ExceptionInterface $e) { + throw new RuntimeException(sprintf( + 'Can not get clipboard: %s', + $e->getMessage() + )); + } + } +} diff --git a/src/Command/Custom/AssertFileDownloadedCommand.php b/src/Command/Custom/AssertFileDownloadedCommand.php new file mode 100644 index 00000000..6b9891d7 --- /dev/null +++ b/src/Command/Custom/AssertFileDownloadedCommand.php @@ -0,0 +1,42 @@ +httpClient->request( + 'GET', + sprintf('%s/%s', $this->getUrl('download', $driver), $target) + )->getStatusCode(); + $this->assert(200 === $code, sprintf( + 'Failed expecting that file %s is downloaded', + $target + )); + } catch (ExceptionInterface $e) { + throw new RuntimeException(sprintf( + 'Can not get downloaded file %s: %s', + $target, + $e->getMessage() + )); + } + } +} diff --git a/src/Command/Custom/UpdateClipboardCommand.php b/src/Command/Custom/UpdateClipboardCommand.php new file mode 100644 index 00000000..200f5fd3 --- /dev/null +++ b/src/Command/Custom/UpdateClipboardCommand.php @@ -0,0 +1,38 @@ +httpClient->request( + 'POST', + $this->getUrl('clipboard', $driver), + ['body' => $target] + )->getStatusCode(); + } catch (ExceptionInterface $e) { + throw new RuntimeException(sprintf( + 'Can not update clipboard: %s', + $e->getMessage() + )); + } + } +} diff --git a/src/Command/Custom/UploadCommand.php b/src/Command/Custom/UploadCommand.php new file mode 100644 index 00000000..72da07d1 --- /dev/null +++ b/src/Command/Custom/UploadCommand.php @@ -0,0 +1,48 @@ +getFilePath($value)); + } + + public function run(?string $target, ?string $value, ValuesInterface $values, RemoteWebDriver $driver): void + { + parent::run($target, $value, $values, $driver); + $driver + ->findElement($this->getSelector($target)) + ->setFileDetector(new LocalFileDetector()) + ->sendKeys($this->getFilePath($value)); + } + + protected function getFilePath(string $value): string + { + return $this->uploadDir . DIRECTORY_SEPARATOR . $value; + } +} diff --git a/src/Command/Keyboard/AbstractKeyboardCommand.php b/src/Command/Keyboard/AbstractKeyboardCommand.php new file mode 100644 index 00000000..b96ee109 --- /dev/null +++ b/src/Command/Keyboard/AbstractKeyboardCommand.php @@ -0,0 +1,18 @@ +findElement($this->getSelector($target)) + ->click() + ->sendKeys($value); + } +} diff --git a/src/Command/Keyboard/TypeCommand.php b/src/Command/Keyboard/TypeCommand.php new file mode 100644 index 00000000..2e653e5e --- /dev/null +++ b/src/Command/Keyboard/TypeCommand.php @@ -0,0 +1,34 @@ +findElement($this->getSelector($target)) + ->click() + ->clear() + ->sendKeys($value); + } +} diff --git a/src/Command/Mouse/AbstractMouseCommand.php b/src/Command/Mouse/AbstractMouseCommand.php new file mode 100644 index 00000000..9e2dd6e7 --- /dev/null +++ b/src/Command/Mouse/AbstractMouseCommand.php @@ -0,0 +1,13 @@ +getSelect($driver->findElement($this->getSelector($target))); + if (str_starts_with($value, 'index=')) { + $select->selectByIndex((int) substr($value, 6)); + } elseif (str_starts_with($value, 'value=')) { + $select->selectByValue(substr($value, 6)); + } elseif (str_starts_with($value, 'label=')) { + $select->selectByVisibleText(substr($value, 6)); + } + } +} diff --git a/src/Command/Mouse/CheckCommand.php b/src/Command/Mouse/CheckCommand.php new file mode 100644 index 00000000..f55f482d --- /dev/null +++ b/src/Command/Mouse/CheckCommand.php @@ -0,0 +1,28 @@ +findElement($this->getSelector($target)); + if (!$element->isSelected()) { + $element->click(); + } + } +} diff --git a/src/Command/Mouse/ClickAtCommand.php b/src/Command/Mouse/ClickAtCommand.php new file mode 100644 index 00000000..e9085101 --- /dev/null +++ b/src/Command/Mouse/ClickAtCommand.php @@ -0,0 +1,30 @@ +getPoint($value); + $driver->action()->moveToElement( + $driver->findElement($this->getSelector($target)), + $point->getX(), + $point->getY() + )->click()->perform(); + } +} diff --git a/src/Command/Mouse/ClickCommand.php b/src/Command/Mouse/ClickCommand.php new file mode 100644 index 00000000..61d6ec82 --- /dev/null +++ b/src/Command/Mouse/ClickCommand.php @@ -0,0 +1,25 @@ +findElement($this->getSelector($target))->click(); + } +} diff --git a/src/Command/Mouse/DoubleClickAtCommand.php b/src/Command/Mouse/DoubleClickAtCommand.php new file mode 100644 index 00000000..e85d07ce --- /dev/null +++ b/src/Command/Mouse/DoubleClickAtCommand.php @@ -0,0 +1,30 @@ +getPoint($value); + $driver->action()->moveToElement( + $driver->findElement($this->getSelector($target)), + $point->getX(), + $point->getY() + )->doubleClick()->perform(); + } +} diff --git a/src/Command/Mouse/DoubleClickCommand.php b/src/Command/Mouse/DoubleClickCommand.php new file mode 100644 index 00000000..51821d3f --- /dev/null +++ b/src/Command/Mouse/DoubleClickCommand.php @@ -0,0 +1,27 @@ +action()->doubleClick( + $driver->findElement($this->getSelector($target)) + )->perform(); + } +} diff --git a/src/Command/Mouse/DragAndDropToObjectCommand.php b/src/Command/Mouse/DragAndDropToObjectCommand.php new file mode 100644 index 00000000..97b0dfd6 --- /dev/null +++ b/src/Command/Mouse/DragAndDropToObjectCommand.php @@ -0,0 +1,43 @@ +action()->dragAndDrop( + $driver->findElement($this->getSelector($target)), + $driver->findElement($this->getSelector($value)) + )->perform(); + } +} diff --git a/src/Command/Mouse/MouseDownAtCommand.php b/src/Command/Mouse/MouseDownAtCommand.php new file mode 100644 index 00000000..25e9d9b8 --- /dev/null +++ b/src/Command/Mouse/MouseDownAtCommand.php @@ -0,0 +1,30 @@ +getPoint($value); + $driver->getMouse()->mouseMove( + $driver->findElement($this->getSelector($target))->getCoordinates(), + $point->getX(), + $point->getY() + )->mouseDown(); + } +} diff --git a/src/Command/Mouse/MouseDownCommand.php b/src/Command/Mouse/MouseDownCommand.php new file mode 100644 index 00000000..ed1b61f6 --- /dev/null +++ b/src/Command/Mouse/MouseDownCommand.php @@ -0,0 +1,27 @@ +getMouse()->mouseDown( + $driver->findElement($this->getSelector($target))->getCoordinates() + ); + } +} diff --git a/src/Command/Mouse/MouseMoveAtCommand.php b/src/Command/Mouse/MouseMoveAtCommand.php new file mode 100644 index 00000000..0367b309 --- /dev/null +++ b/src/Command/Mouse/MouseMoveAtCommand.php @@ -0,0 +1,30 @@ +getPoint($value); + $driver->getMouse()->mouseMove( + $driver->findElement($this->getSelector($target))->getCoordinates(), + $point->getX(), + $point->getY() + ); + } +} diff --git a/src/Command/Mouse/MouseOutCommand.php b/src/Command/Mouse/MouseOutCommand.php new file mode 100644 index 00000000..cb78ced2 --- /dev/null +++ b/src/Command/Mouse/MouseOutCommand.php @@ -0,0 +1,52 @@ +findElement($this->getSelector($target)); + [$rect, $vp] = $driver->executeScript( + // phpcs:ignore Generic.Files.LineLength + 'return [arguments[0].getBoundingClientRect(), {height: window.innerHeight, width: window.innerWidth}];', + [$element] + ); + if ($rect->top > 0) { + // try top + $y = -($rect->height / 2 + 1); + $driver->getMouse()->mouseMove($element->getCoordinates(), null, $y); + } elseif ($vp->width > $rect->right) { + // try right + $x = $rect->right / 2 + 1; + $driver->getMouse()->mouseMove($element->getCoordinates(), $x); + } elseif ($vp->height > $rect->bottom) { + // try bottom + $y = $rect->height / 2 + 1; + $driver->getMouse()->mouseMove($element->getCoordinates(), null, $y); + } elseif ($rect->left > 0) { + // try left + $x = (int) (-($rect->right / 2)); + $driver->getMouse()->mouseMove($element->getCoordinates(), $x); + } else { + throw new Exception( + 'Unable to perform mouse out as the element takes up the entire viewport' + ); + } + } +} diff --git a/src/Command/Mouse/MouseOverCommand.php b/src/Command/Mouse/MouseOverCommand.php new file mode 100644 index 00000000..a6368411 --- /dev/null +++ b/src/Command/Mouse/MouseOverCommand.php @@ -0,0 +1,27 @@ +getMouse()->mouseMove( + $driver->findElement($this->getSelector($target))->getCoordinates() + ); + } +} diff --git a/src/Command/Mouse/MouseUpAtCommand.php b/src/Command/Mouse/MouseUpAtCommand.php new file mode 100644 index 00000000..2c12a29b --- /dev/null +++ b/src/Command/Mouse/MouseUpAtCommand.php @@ -0,0 +1,30 @@ +getPoint($value); + $driver->getMouse()->mouseMove( + $driver->findElement($this->getSelector($target))->getCoordinates(), + $point->getX(), + $point->getY() + )->mouseUp(); + } +} diff --git a/src/Command/Mouse/MouseUpCommand.php b/src/Command/Mouse/MouseUpCommand.php new file mode 100644 index 00000000..fc57dee5 --- /dev/null +++ b/src/Command/Mouse/MouseUpCommand.php @@ -0,0 +1,27 @@ +getMouse()->mouseUp( + $driver->findElement($this->getSelector($target))->getCoordinates() + ); + } +} diff --git a/src/Command/Mouse/RemoveSelectionCommand.php b/src/Command/Mouse/RemoveSelectionCommand.php new file mode 100644 index 00000000..28acda3d --- /dev/null +++ b/src/Command/Mouse/RemoveSelectionCommand.php @@ -0,0 +1,37 @@ +getSelect($driver->findElement($this->getSelector($target))); + if (str_starts_with($value, 'index=')) { + $select->deselectByIndex((int) substr($value, 6)); + } elseif (str_starts_with($value, 'value=')) { + $select->deselectByValue(substr($value, 6)); + } elseif (str_starts_with($value, 'label=')) { + $select->deselectByVisibleText(substr($value, 6)); + } + } +} diff --git a/src/Command/Mouse/SelectCommand.php b/src/Command/Mouse/SelectCommand.php new file mode 100644 index 00000000..7c46d599 --- /dev/null +++ b/src/Command/Mouse/SelectCommand.php @@ -0,0 +1,43 @@ +findElement($this->getSelector($target)) + ->findElement($this->getSelector($value)) + ->click(); + } +} diff --git a/src/Command/Mouse/UncheckCommand.php b/src/Command/Mouse/UncheckCommand.php new file mode 100644 index 00000000..9e18ab8d --- /dev/null +++ b/src/Command/Mouse/UncheckCommand.php @@ -0,0 +1,28 @@ +findElement($this->getSelector($target)); + if ($element->isSelected()) { + $element->click(); + } + } +} diff --git a/src/Command/Runner/AlertCommandRunner.php b/src/Command/Runner/AlertCommandRunner.php deleted file mode 100644 index 309a2136..00000000 --- a/src/Command/Runner/AlertCommandRunner.php +++ /dev/null @@ -1,68 +0,0 @@ -getCommand()) { - case self::ACCEPT_ALERT: - case self::ACCEPT_CONFIRMATION: - $driver->switchTo()->alert()->accept(); - break; - case self::ANSWER_PROMPT: - $alert = $driver->switchTo()->alert(); - if ($command->getTarget()) { - $alert->sendKeys($command->getTarget()); - } - $alert->accept(); - break; - case self::DISMISS_CONFIRMATION: - case self::DISMISS_PROMPT: - $driver->switchTo()->alert()->dismiss(); - break; - default: - break; - } - } - - public function validateTarget(CommandInterface $command): bool - { - return true; - } -} diff --git a/src/Command/Runner/AssertionRunner.php b/src/Command/Runner/AssertionRunner.php deleted file mode 100644 index 7ca303b1..00000000 --- a/src/Command/Runner/AssertionRunner.php +++ /dev/null @@ -1,227 +0,0 @@ -getAllCommands(); - } - - public function getCommandsRequireValue(): array - { - return [ - self::ASSERT, - self::ASSERT_TEXT, - self::ASSERT_NOT_TEXT, - self::ASSERT_VALUE, - self::ASSERT_SELECTED_VALUE, - self::ASSERT_NOT_SELECTED_VALUE, - self::ASSERT_SELECTED_LABEL, - self::ASSERT_NOT_SELECTED_LABEL, - ]; - } - - public function run(CommandInterface $command, ColorInterface $color, RemoteWebDriver $driver): void - { - switch ($command->getCommand()) { - case self::ASSERT: - $actual = $color->getValue($command->getTarget()); - $this->assert( - $actual === $command->getValue(), - sprintf('Actual value "%s" did not match "%s"', $actual, $command->getValue()) - ); - break; - case self::ASSERT_ALERT: - case self::ASSERT_CONFIRMATION: - case self::ASSERT_PROMPT: - $alertText = $driver->switchTo()->alert()->getText(); - $type = [ - self::ASSERT_ALERT => 'alert', - self::ASSERT_CONFIRMATION => 'confirm', - self::ASSERT_PROMPT => 'prompt', - ][$command->getCommand()]; - $this->assert( - $alertText === $command->getTarget(), - sprintf('Actual %s text "%s" did not match "%s"', $type, $alertText, $command->getTarget()) - ); - break; - case self::ASSERT_TITLE: - $this->assert( - $driver->getTitle() === $command->getTarget(), - sprintf('Actual title "%s" did not match "%s"', $driver->getTitle(), $command->getTarget()) - ); - break; - case self::ASSERT_TEXT: - $elementText = $driver->findElement($this->getSelector($command->getTarget()))->getText(); - $this->assert( - $elementText === $command->getValue(), - sprintf('Actual text "%s" did not match "%s"', $elementText, $command->getValue()) - ); - break; - case self::ASSERT_NOT_TEXT: - $elementText = $driver->findElement($this->getSelector($command->getTarget()))->getText(); - $this->assert( - $elementText !== $command->getValue(), - sprintf('Actual text "%s" did match "%s"', $elementText, $command->getValue()) - ); - break; - case self::ASSERT_VALUE: - $elementValue = $driver->findElement($this->getSelector($command->getTarget()))->getAttribute('value'); - $this->assert( - $elementValue === $command->getValue(), - sprintf('Actual value "%s" did not match "%s"', $elementValue, $command->getValue()) - ); - break; - case self::ASSERT_EDITABLE: - $this->assert( - $this->isElementEditable($driver, $driver->findElement($this->getSelector($command->getTarget()))), - sprintf('Element "%s" is not editable', $command->getTarget()) - ); - break; - case self::ASSERT_NOT_EDITABLE: - $this->assert( - !$this->isElementEditable($driver, $driver->findElement($this->getSelector($command->getTarget()))), - sprintf('Element "%s" is editable', $command->getTarget()) - ); - break; - case self::ASSERT_ELEMENT_PRESENT: - $this->assert( - count($driver->findElements($this->getSelector($command->getTarget()))) > 0, - sprintf('Expected element "%s" was not found in page', $command->getTarget()) - ); - break; - case self::ASSERT_ELEMENT_NOT_PRESENT: - $this->assert( - 0 === count($driver->findElements($this->getSelector($command->getTarget()))), - sprintf('Unexpected element "%s" was found in page', $command->getTarget()) - ); - break; - case self::ASSERT_CHECKED: - $this->assert( - $driver->findElement($this->getSelector($command->getTarget()))->isSelected(), - sprintf('Element "%s" is not checked, expected to be checked', $command->getTarget()) - ); - break; - case self::ASSERT_NOT_CHECKED: - $this->assert( - !$driver->findElement($this->getSelector($command->getTarget()))->isSelected(), - sprintf('Element "%s" is checked, expected to be unchecked', $command->getTarget()) - ); - break; - case self::ASSERT_SELECTED_VALUE: - $select = $this->getSelect($driver->findElement($this->getSelector($command->getTarget()))); - $elementValue = $select->getFirstSelectedOption()->getAttribute('value'); - $this->assert( - $elementValue === $command->getValue(), - sprintf('Actual value "%s" did not match "%s"', $elementValue, $command->getValue()) - ); - break; - case self::ASSERT_NOT_SELECTED_VALUE: - $select = $this->getSelect($driver->findElement($this->getSelector($command->getTarget()))); - $elementValue = $select->getFirstSelectedOption()->getAttribute('value'); - $this->assert( - $elementValue !== $command->getValue(), - sprintf('Actual value "%s" did match "%s"', $elementValue, $command->getValue()) - ); - break; - case self::ASSERT_SELECTED_LABEL: - $select = $this->getSelect($driver->findElement($this->getSelector($command->getTarget()))); - $elementLabel = $select->getFirstSelectedOption()->getText(); - $this->assert( - $elementLabel === $command->getValue(), - sprintf('Actual label "%s" did not match "%s"', $elementLabel, $command->getValue()) - ); - break; - case self::ASSERT_NOT_SELECTED_LABEL: - $select = $this->getSelect($driver->findElement($this->getSelector($command->getTarget()))); - $elementLabel = $select->getFirstSelectedOption()->getText(); - $this->assert( - $elementLabel !== $command->getValue(), - sprintf('Actual label "%s" did match "%s"', $elementLabel, $command->getValue()) - ); - break; - default: - break; - } - } - - public function validateTarget(CommandInterface $command): bool - { - switch ($command->getCommand()) { - case self::ASSERT_TEXT: - case self::ASSERT_NOT_TEXT: - case self::ASSERT_VALUE: - case self::ASSERT_EDITABLE: - case self::ASSERT_NOT_EDITABLE: - case self::ASSERT_ELEMENT_PRESENT: - case self::ASSERT_ELEMENT_NOT_PRESENT: - case self::ASSERT_CHECKED: - case self::ASSERT_NOT_CHECKED: - case self::ASSERT_SELECTED_VALUE: - case self::ASSERT_NOT_SELECTED_VALUE: - case self::ASSERT_SELECTED_LABEL: - case self::ASSERT_NOT_SELECTED_LABEL: - return $command->getTarget() && $this->isValidSelector($command->getTarget()); - default: - return true; - } - } - - protected function assert(bool $assertion, string $message): void - { - if (!$assertion) { - throw new Exception($message); - } - } -} diff --git a/src/Command/Runner/KeyboardCommandRunner.php b/src/Command/Runner/KeyboardCommandRunner.php deleted file mode 100644 index 2c984f7d..00000000 --- a/src/Command/Runner/KeyboardCommandRunner.php +++ /dev/null @@ -1,66 +0,0 @@ -getAllCommands(); - } - - public function getCommandsRequireValue(): array - { - return []; - } - - public function run(CommandInterface $command, ColorInterface $color, RemoteWebDriver $driver): void - { - switch ($command->getCommand()) { - case self::TYPE: - $driver - ->findElement($this->getSelector($command->getTarget())) - ->click() - ->clear() - ->sendKeys($this->sanitizeValue($command)); - break; - case self::SEND_KEYS: - $driver - ->findElement($this->getSelector($command->getTarget())) - ->click() - ->sendKeys($this->sanitizeValue($command)); - break; - default: - break; - } - } - - public function validateTarget(CommandInterface $command): bool - { - return $command->getTarget() && $this->isValidSelector($command->getTarget()); - } - - /** - * Don't allow to upload local file. - */ - protected function sanitizeValue(CommandInterface $command): array - { - return [(string) $command->getValue()]; - } -} diff --git a/src/Command/Runner/MouseCommandRunner.php b/src/Command/Runner/MouseCommandRunner.php deleted file mode 100644 index b379b765..00000000 --- a/src/Command/Runner/MouseCommandRunner.php +++ /dev/null @@ -1,233 +0,0 @@ -getAllCommands(); - } - - public function getCommandsRequireValue(): array - { - return [ - self::ADD_SELECTION, - self::REMOVE_SELECTION, - self::CLICK_AT, - self::DOUBLE_CLICK_AT, - self::DRAG_AND_DROP_TO_OBJECT, - self::MOUSE_DOWN_AT, - self::MOUSE_MOVE_AT, - self::MOUSE_UP_AT, - self::SELECT, - ]; - } - - public function run(CommandInterface $command, ColorInterface $color, RemoteWebDriver $driver): void - { - switch ($command->getCommand()) { - case self::ADD_SELECTION: - $select = $this->getSelect($driver->findElement($this->getSelector($command->getTarget()))); - if (str_starts_with($command->getValue(), 'index=')) { - $select->selectByIndex((int) substr($command->getValue(), 6)); - } elseif (str_starts_with($command->getValue(), 'value=')) { - $select->selectByValue(substr($command->getValue(), 6)); - } elseif (str_starts_with($command->getValue(), 'label=')) { - $select->selectByVisibleText(substr($command->getValue(), 6)); - } - break; - case self::REMOVE_SELECTION: - $select = $this->getSelect($driver->findElement($this->getSelector($command->getTarget()))); - if (str_starts_with($command->getValue(), 'index=')) { - $select->deselectByIndex((int) substr($command->getValue(), 6)); - } elseif (str_starts_with($command->getValue(), 'value=')) { - $select->deselectByValue(substr($command->getValue(), 6)); - } elseif (str_starts_with($command->getValue(), 'label=')) { - $select->deselectByVisibleText(substr($command->getValue(), 6)); - } - break; - case self::CHECK: - $element = $driver->findElement($this->getSelector($command->getTarget())); - if (!$element->isSelected()) { - $element->click(); - } - break; - case self::UNCHECK: - $element = $driver->findElement($this->getSelector($command->getTarget())); - if ($element->isSelected()) { - $element->click(); - } - break; - case self::CLICK: - $driver->findElement($this->getSelector($command->getTarget()))->click(); - break; - case self::CLICK_AT: - $point = $this->getPoint($command->getValue()); - $driver->action()->moveToElement( - $driver->findElement($this->getSelector($command->getTarget())), - $point->getX(), - $point->getY() - )->click()->perform(); - break; - case self::DOUBLE_CLICK: - $driver->action()->doubleClick( - $driver->findElement($this->getSelector($command->getTarget())) - )->perform(); - break; - case self::DOUBLE_CLICK_AT: - $point = $this->getPoint($command->getValue()); - $driver->action()->moveToElement( - $driver->findElement($this->getSelector($command->getTarget())), - $point->getX(), - $point->getY() - )->doubleClick()->perform(); - break; - case self::DRAG_AND_DROP_TO_OBJECT: - $driver->action()->dragAndDrop( - $driver->findElement($this->getSelector($command->getTarget())), - $driver->findElement($this->getSelector($command->getValue())) - )->perform(); - break; - case self::MOUSE_DOWN: - $driver->getMouse()->mouseDown( - $driver->findElement($this->getSelector($command->getTarget()))->getCoordinates() - ); - break; - case self::MOUSE_DOWN_AT: - $point = $this->getPoint($command->getValue()); - $driver->getMouse()->mouseMove( - $driver->findElement($this->getSelector($command->getTarget()))->getCoordinates(), - $point->getX(), - $point->getY() - )->mouseDown(); - break; - case self::MOUSE_MOVE_AT: - $point = $this->getPoint($command->getValue()); - $driver->getMouse()->mouseMove( - $driver->findElement($this->getSelector($command->getTarget()))->getCoordinates(), - $point->getX(), - $point->getY() - ); - break; - case self::MOUSE_OUT: - $element = $driver->findElement($this->getSelector($command->getTarget())); - [$rect, $vp] = $driver->executeScript( - // phpcs:ignore Generic.Files.LineLength - 'return [arguments[0].getBoundingClientRect(), {height: window.innerHeight, width: window.innerWidth}];', - [$element] - ); - if ($rect->top > 0) { - // try top - $y = -($rect->height / 2 + 1); - $driver->getMouse()->mouseMove($element->getCoordinates(), null, $y); - break; - } elseif ($vp->width > $rect->right) { - // try right - $x = $rect->right / 2 + 1; - $driver->getMouse()->mouseMove($element->getCoordinates(), $x); - break; - } elseif ($vp->height > $rect->bottom) { - // try bottom - $y = $rect->height / 2 + 1; - $driver->getMouse()->mouseMove($element->getCoordinates(), null, $y); - break; - } elseif ($rect->left > 0) { - // try left - $x = (int) (-($rect->right / 2)); - $driver->getMouse()->mouseMove($element->getCoordinates(), $x); - break; - } - - throw new Exception( - 'Unable to perform mouse out as the element takes up the entire viewport' - ); - case self::MOUSE_OVER: - $driver->getMouse()->mouseMove( - $driver->findElement($this->getSelector($command->getTarget()))->getCoordinates() - ); - break; - case self::MOUSE_UP: - $driver->getMouse()->mouseUp( - $driver->findElement($this->getSelector($command->getTarget()))->getCoordinates() - ); - break; - case self::MOUSE_UP_AT: - $point = $this->getPoint($command->getValue()); - $driver->getMouse()->mouseMove( - $driver->findElement($this->getSelector($command->getTarget()))->getCoordinates(), - $point->getX(), - $point->getY() - )->mouseUp(); - break; - case self::SELECT: - $driver - ->findElement($this->getSelector($command->getTarget())) - ->findElement($this->getSelector($command->getValue())) - ->click(); - break; - default: - break; - } - } - - public function validateTarget(CommandInterface $command): bool - { - return $command->getTarget() && $this->isValidSelector($command->getTarget()); - } - - protected function getPoint(string $target): WebDriverPoint - { - list($x, $y) = explode(',', $target); - - return new WebDriverPoint((int) $x, (int) $y); - } -} diff --git a/src/Command/Runner/ScriptCommandRunner.php b/src/Command/Runner/ScriptCommandRunner.php deleted file mode 100644 index db1970db..00000000 --- a/src/Command/Runner/ScriptCommandRunner.php +++ /dev/null @@ -1,65 +0,0 @@ -getAllCommands(); - } - - public function getCommandsRequireValue(): array - { - return [ - self::EXECUTE_SCRIPT, - self::EXECUTE_ASYNC_SCRIPT, - ]; - } - - public function run(CommandInterface $command, ColorInterface $color, RemoteWebDriver $driver): void - { - switch ($command->getCommand()) { - case self::RUN_SCRIPT: - $driver->executeScript($command->getTarget()); - break; - case self::EXECUTE_SCRIPT: - $value = $driver->executeScript($command->getTarget()); - if ($command->getValue()) { - $color->setValue($command->getValue(), $value); - } - break; - case self::EXECUTE_ASYNC_SCRIPT: - $value = $driver->executeAsyncScript($command->getTarget()); - if ($command->getValue()) { - $color->setValue($command->getValue(), $value); - } - break; - default: - break; - } - } - - public function validateTarget(CommandInterface $command): bool - { - return true; - } -} diff --git a/src/Command/Runner/StoreCommandRunner.php b/src/Command/Runner/StoreCommandRunner.php deleted file mode 100644 index 78ea80e5..00000000 --- a/src/Command/Runner/StoreCommandRunner.php +++ /dev/null @@ -1,138 +0,0 @@ -getCommand()) { - case self::STORE: - $color->setValue($command->getValue(), $command->getTarget()); - break; - case self::STORE_ATTRIBUTE: - list($elementLocator, $attributeName) = explode('@', $command->getTarget(), 2); - $color->setValue( - $command->getValue(), - $driver->findElement($this->getSelector($elementLocator))->getAttribute($attributeName) - ); - break; - case self::STORE_ELEMENT_COUNT: - $color->setValue( - $command->getValue(), - count($driver->findElements($this->getSelector($command->getTarget()))) - ); - break; - case self::STORE_JSON: - $color->setValue( - $command->getValue(), - json_decode($command->getTarget()) - ); - break; - case self::STORE_TEXT: - $color->setValue( - $command->getValue(), - $driver->findElement($this->getSelector($command->getTarget()))->getText() - ); - break; - case self::STORE_TITLE: - $color->setValue($command->getTarget(), $driver->getTitle()); - break; - case self::STORE_VALUE: - $color->setValue( - $command->getValue(), - $driver->findElement($this->getSelector($command->getTarget()))->getAttribute('value') - ); - break; - case self::STORE_WINDOW_HANDLE: - $color->setValue($command->getTarget(), $driver->getWindowHandle()); - break; - default: - break; - } - } - - public function validateTarget(CommandInterface $command): bool - { - switch ($command->getCommand()) { - case self::STORE_ATTRIBUTE: - return $command->getTarget() && $this->isValidAttribute($command->getTarget()); - case self::STORE_ELEMENT_COUNT: - case self::STORE_TEXT: - case self::STORE_VALUE: - return $command->getTarget() && $this->isValidSelector($command->getTarget()); - case self::STORE_JSON: - return $command->getTarget() && $this->isValidJson($command->getTarget()); - default: - return true; - } - } - - protected function isValidAttribute(string $target): bool - { - list($elementLocator) = explode('@', $target, 2); - - return $this->isValidSelector($elementLocator); - } - - protected function isValidJson(string $target): bool - { - json_decode($target); - - return JSON_ERROR_NONE === json_last_error(); - } -} diff --git a/src/Command/Runner/WaitCommandRunner.php b/src/Command/Runner/WaitCommandRunner.php deleted file mode 100644 index 069c18bc..00000000 --- a/src/Command/Runner/WaitCommandRunner.php +++ /dev/null @@ -1,95 +0,0 @@ -getAllCommands(); - } - - public function getCommandsRequireValue(): array - { - return $this->getAllCommands(); - } - - public function run(CommandInterface $command, ColorInterface $color, RemoteWebDriver $driver): void - { - switch ($command->getCommand()) { - case self::WAIT_FOR_ELEMENT_EDITABLE: - $driver->wait((int) $command->getValue())->until( - fn () => $this->isElementEditable( - $driver, - $driver->findElement($this->getSelector($command->getTarget())) - ), - 'Timed out waiting for element to be editable' - ); - break; - case self::WAIT_FOR_ELEMENT_NOT_EDITABLE: - $driver->wait((int) $command->getValue())->until( - fn () => !$this->isElementEditable( - $driver, - $driver->findElement($this->getSelector($command->getTarget())) - ), - 'Timed out waiting for element to not be editable' - ); - break; - case self::WAIT_FOR_ELEMENT_PRESENT: - $driver->wait((int) $command->getValue())->until( - WebDriverExpectedCondition::presenceOfElementLocated($this->getSelector($command->getTarget())) - ); - break; - case self::WAIT_FOR_ELEMENT_NOT_PRESENT: - $elements = $driver->findElements($this->getSelector($command->getTarget())); - if (count($elements) > 0) { - $driver->wait((int) $command->getValue())->until( - WebDriverExpectedCondition::stalenessOf($elements[0]) - ); - } - break; - case self::WAIT_FOR_ELEMENT_VISIBLE: - $driver->wait((int) $command->getValue())->until( - WebDriverExpectedCondition::visibilityOfElementLocated($this->getSelector($command->getTarget())) - ); - break; - case self::WAIT_FOR_ELEMENT_NOT_VISIBLE: - $driver->wait((int) $command->getValue())->until( - WebDriverExpectedCondition::invisibilityOfElementLocated($this->getSelector($command->getTarget())) - ); - break; - default: - break; - } - } - - public function validateTarget(CommandInterface $command): bool - { - return $command->getTarget() && $this->isValidSelector($command->getTarget()); - } -} diff --git a/src/Command/Runner/WindowCommandRunner.php b/src/Command/Runner/WindowCommandRunner.php deleted file mode 100644 index e5a7d03e..00000000 --- a/src/Command/Runner/WindowCommandRunner.php +++ /dev/null @@ -1,138 +0,0 @@ -getCommand()) { - case self::OPEN: - $driver->get($command->getTarget()); - break; - case self::SET_WINDOW_SIZE: - $driver->manage()->window()->setSize($this->getDimension($command->getTarget())); - break; - case self::SELECT_WINDOW: - $driver->switchTo()->window($this->getHandle($command->getTarget())); - break; - case self::CLOSE: - $driver->close(); - break; - case self::SELECT_FRAME: - $targetLocator = $driver->switchTo(); - if ('relative=top' === $command->getTarget()) { - $targetLocator->defaultContent(); - } elseif ('relative=parent' === $command->getTarget()) { - $targetLocator->parent(); - } elseif (str_starts_with($command->getTarget(), 'index=')) { - $targetLocator->frame((int) substr($command->getTarget(), 6)); - } else { - $webDriverBy = $this->getSelector($command->getTarget()); - $driver->wait()->until( - WebDriverExpectedCondition::presenceOfElementLocated($webDriverBy) - ); - $targetLocator->frame($driver->findElement($webDriverBy)); - } - break; - default: - break; - } - } - - public function validateTarget(CommandInterface $command): bool - { - switch ($command->getCommand()) { - case self::OPEN: - return $command->getTarget() && $this->isValidUrl($command->getTarget()); - case self::SET_WINDOW_SIZE: - return $command->getTarget() && 2 === count(explode('x', $command->getTarget())); - case self::SELECT_WINDOW: - return $command->getTarget() && $this->isValidHandle($command->getTarget()); - case self::SELECT_FRAME: - return $command->getTarget() && $this->isValidFrame($command->getTarget()); - default: - return true; - } - } - - protected function getDimension(string $target): WebDriverDimension - { - list($width, $height) = explode('x', $target); - - return new WebDriverDimension((int) $width, (int) $height); - } - - protected function getHandle(string $target): string - { - if (!$this->isValidHandle($target)) { - throw new Exception('Invalid window handle given (e.g. handle=${handleVariable})'); - } - - return substr($target, 7); - } - - protected function isValidHandle(string $target): bool - { - return str_starts_with($target, 'handle='); - } - - protected function isValidFrame(string $target): bool - { - return $target && ( - in_array($target, ['relative=top', 'relative=parent']) - || str_starts_with($target, 'index=') - || $this->isValidSelector($target) - ); - } - - /** - * TODO Find a solution better than this. - */ - protected function isValidUrl(string $target): bool - { - $url = filter_var($target, FILTER_SANITIZE_URL); - - return filter_var($url, FILTER_VALIDATE_URL); - } -} diff --git a/src/Command/Script/AbstractScriptCommand.php b/src/Command/Script/AbstractScriptCommand.php new file mode 100644 index 00000000..950558d8 --- /dev/null +++ b/src/Command/Script/AbstractScriptCommand.php @@ -0,0 +1,18 @@ +executeAsyncScript($target); + if ($value) { + $values->setValue($value, $result); + } + } +} diff --git a/src/Command/Script/ExecuteScriptCommand.php b/src/Command/Script/ExecuteScriptCommand.php new file mode 100644 index 00000000..1ef25fa4 --- /dev/null +++ b/src/Command/Script/ExecuteScriptCommand.php @@ -0,0 +1,38 @@ +executeScript($target); + if ($value) { + $values->setValue($value, $result); + } + } +} diff --git a/src/Command/Script/RunScriptCommand.php b/src/Command/Script/RunScriptCommand.php new file mode 100644 index 00000000..d251407b --- /dev/null +++ b/src/Command/Script/RunScriptCommand.php @@ -0,0 +1,30 @@ +executeScript($target); + } +} diff --git a/src/Command/Store/AbstractStoreCommand.php b/src/Command/Store/AbstractStoreCommand.php new file mode 100644 index 00000000..a6235f2a --- /dev/null +++ b/src/Command/Store/AbstractStoreCommand.php @@ -0,0 +1,13 @@ +setValue( + $value, + $driver->findElement($this->getSelector($elementLocator))->getAttribute($attributeName) + ); + } +} diff --git a/src/Command/Store/StoreCommand.php b/src/Command/Store/StoreCommand.php new file mode 100644 index 00000000..b16ef627 --- /dev/null +++ b/src/Command/Store/StoreCommand.php @@ -0,0 +1,45 @@ +setValue($value, $target); + } +} diff --git a/src/Command/Store/StoreElementCountCommand.php b/src/Command/Store/StoreElementCountCommand.php new file mode 100644 index 00000000..cbb29196 --- /dev/null +++ b/src/Command/Store/StoreElementCountCommand.php @@ -0,0 +1,38 @@ +setValue( + $value, + count($driver->findElements($this->getSelector($target))) + ); + } +} diff --git a/src/Command/Store/StoreJsonCommand.php b/src/Command/Store/StoreJsonCommand.php new file mode 100644 index 00000000..b682d7d6 --- /dev/null +++ b/src/Command/Store/StoreJsonCommand.php @@ -0,0 +1,52 @@ +setValue($value, json_decode($target)); + } +} diff --git a/src/Command/Store/StoreTextCommand.php b/src/Command/Store/StoreTextCommand.php new file mode 100644 index 00000000..620366b1 --- /dev/null +++ b/src/Command/Store/StoreTextCommand.php @@ -0,0 +1,38 @@ +setValue( + $value, + $driver->findElement($this->getSelector($target))->getText() + ); + } +} diff --git a/src/Command/Store/StoreTitleCommand.php b/src/Command/Store/StoreTitleCommand.php new file mode 100644 index 00000000..4e40e557 --- /dev/null +++ b/src/Command/Store/StoreTitleCommand.php @@ -0,0 +1,35 @@ +setValue($target, $driver->getTitle()); + } +} diff --git a/src/Command/Store/StoreValueCommand.php b/src/Command/Store/StoreValueCommand.php new file mode 100644 index 00000000..0bfe1eba --- /dev/null +++ b/src/Command/Store/StoreValueCommand.php @@ -0,0 +1,38 @@ +setValue( + $value, + $driver->findElement($this->getSelector($target))->getAttribute('value') + ); + } +} diff --git a/src/Command/Store/StoreWindowHandleCommand.php b/src/Command/Store/StoreWindowHandleCommand.php new file mode 100644 index 00000000..c7a64fb4 --- /dev/null +++ b/src/Command/Store/StoreWindowHandleCommand.php @@ -0,0 +1,35 @@ +setValue($target, $driver->getWindowHandle()); + } +} diff --git a/src/Command/Wait/AbstractWaitCommand.php b/src/Command/Wait/AbstractWaitCommand.php new file mode 100644 index 00000000..e3e8f925 --- /dev/null +++ b/src/Command/Wait/AbstractWaitCommand.php @@ -0,0 +1,33 @@ +wait((int) $value)->until( + fn () => $this->isElementEditable( + $driver, + $driver->findElement($this->getSelector($target)) + ), + 'Timed out waiting for element to be editable' + ); + } +} diff --git a/src/Command/Wait/WaitForElementNotEditableCommand.php b/src/Command/Wait/WaitForElementNotEditableCommand.php new file mode 100644 index 00000000..8a5859fb --- /dev/null +++ b/src/Command/Wait/WaitForElementNotEditableCommand.php @@ -0,0 +1,21 @@ +wait((int) $value)->until( + fn () => !$this->isElementEditable( + $driver, + $driver->findElement($this->getSelector($target)) + ), + 'Timed out waiting for element to not be editable' + ); + } +} diff --git a/src/Command/Wait/WaitForElementNotPresentCommand.php b/src/Command/Wait/WaitForElementNotPresentCommand.php new file mode 100644 index 00000000..c05306b9 --- /dev/null +++ b/src/Command/Wait/WaitForElementNotPresentCommand.php @@ -0,0 +1,21 @@ +findElements($this->getSelector($target)); + if (count($elements) > 0) { + $driver->wait((int) $value)->until( + WebDriverExpectedCondition::stalenessOf($elements[0]) + ); + } + } +} diff --git a/src/Command/Wait/WaitForElementNotVisibleCommand.php b/src/Command/Wait/WaitForElementNotVisibleCommand.php new file mode 100644 index 00000000..4cab1f77 --- /dev/null +++ b/src/Command/Wait/WaitForElementNotVisibleCommand.php @@ -0,0 +1,18 @@ +wait((int) $value)->until( + WebDriverExpectedCondition::invisibilityOfElementLocated($this->getSelector($target)) + ); + } +} diff --git a/src/Command/Wait/WaitForElementPresentCommand.php b/src/Command/Wait/WaitForElementPresentCommand.php new file mode 100644 index 00000000..0f18f7c3 --- /dev/null +++ b/src/Command/Wait/WaitForElementPresentCommand.php @@ -0,0 +1,18 @@ +wait((int) $value)->until( + WebDriverExpectedCondition::presenceOfElementLocated($this->getSelector($target)) + ); + } +} diff --git a/src/Command/Wait/WaitForElementVisibleCommand.php b/src/Command/Wait/WaitForElementVisibleCommand.php new file mode 100644 index 00000000..cdb1de22 --- /dev/null +++ b/src/Command/Wait/WaitForElementVisibleCommand.php @@ -0,0 +1,18 @@ +wait((int) $value)->until( + WebDriverExpectedCondition::visibilityOfElementLocated($this->getSelector($target)) + ); + } +} diff --git a/src/Command/Window/AbstractWindowCommand.php b/src/Command/Window/AbstractWindowCommand.php new file mode 100644 index 00000000..a7fa4de7 --- /dev/null +++ b/src/Command/Window/AbstractWindowCommand.php @@ -0,0 +1,18 @@ +close(); + } +} diff --git a/src/Command/Window/OpenCommand.php b/src/Command/Window/OpenCommand.php new file mode 100644 index 00000000..02bee25b --- /dev/null +++ b/src/Command/Window/OpenCommand.php @@ -0,0 +1,40 @@ +get($target); + } +} diff --git a/src/Command/Window/SelectFrameCommand.php b/src/Command/Window/SelectFrameCommand.php new file mode 100644 index 00000000..c02d48ec --- /dev/null +++ b/src/Command/Window/SelectFrameCommand.php @@ -0,0 +1,53 @@ +switchTo(); + if ('relative=top' === $target) { + $targetLocator->defaultContent(); + } elseif ('relative=parent' === $target) { + $targetLocator->parent(); + } elseif (str_starts_with($target, 'index=')) { + $targetLocator->frame((int) substr($target, 6)); + } else { + $webDriverBy = $this->getSelector($target); + $driver->wait()->until( + WebDriverExpectedCondition::presenceOfElementLocated($webDriverBy) + ); + $targetLocator->frame($driver->findElement($webDriverBy)); + } + } +} diff --git a/src/Command/Window/SelectWindowCommand.php b/src/Command/Window/SelectWindowCommand.php new file mode 100644 index 00000000..7fd048a5 --- /dev/null +++ b/src/Command/Window/SelectWindowCommand.php @@ -0,0 +1,40 @@ +switchTo()->window($this->getHandle($target)); + } +} diff --git a/src/Command/Window/SetWindowSizeCommand.php b/src/Command/Window/SetWindowSizeCommand.php new file mode 100644 index 00000000..952f6351 --- /dev/null +++ b/src/Command/Window/SetWindowSizeCommand.php @@ -0,0 +1,43 @@ +manage()->window()->setSize($this->getDimension($target)); + } +} diff --git a/src/DependencyInjection/Compiler/PluginPass.php b/src/DependencyInjection/Compiler/PluginPass.php index 5cce8f89..22db1de3 100644 --- a/src/DependencyInjection/Compiler/PluginPass.php +++ b/src/DependencyInjection/Compiler/PluginPass.php @@ -14,7 +14,7 @@ class PluginPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { $allPlugins = []; - foreach ($container->findTaggedServiceIds(PluginInterface::TAG, true) as $serviceId => $attributes) { + foreach (array_keys($container->findTaggedServiceIds(PluginInterface::TAG, true)) as $serviceId) { $definition = $container->getDefinition($serviceId); $class = $definition->getClass(); if (is_subclass_of($class, PluginInterface::class) && $class::isSupported()) { diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php deleted file mode 100644 index a97df678..00000000 --- a/src/DependencyInjection/Configuration.php +++ /dev/null @@ -1,29 +0,0 @@ -getRootNode(); - - $rootNode - ->children() - ->scalarNode(static::WEBDRIVER_URI) - ->isRequired() - ->cannotBeEmpty() - ->defaultValue('http://localhost:4444') - ->end() - ->end() - ; - - return $treeBuilder; - } -} diff --git a/src/DependencyInjection/TienvxMbtExtension.php b/src/DependencyInjection/TienvxMbtExtension.php deleted file mode 100644 index 6652d75e..00000000 --- a/src/DependencyInjection/TienvxMbtExtension.php +++ /dev/null @@ -1,50 +0,0 @@ -load('services.php'); - - $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $configs); - - $container->findDefinition(SelenoidHelperInterface::class) - ->addMethodCall('setWebdriverUri', [$config[Configuration::WEBDRIVER_URI]]) - ; - - $this->registerForAutoconfiguration($container); - } - - private function registerForAutoconfiguration(ContainerBuilder $container): void - { - $container->registerForAutoconfiguration(PluginInterface::class) - ->setLazy(true) - ->addTag(PluginInterface::TAG); - $container->registerForAutoconfiguration(CommandRunnerInterface::class) - ->setLazy(true) - ->addTag(CommandRunnerInterface::TAG); - } -} diff --git a/src/Entity/Bug.php b/src/Entity/Bug.php index 10505d75..1159556d 100644 --- a/src/Entity/Bug.php +++ b/src/Entity/Bug.php @@ -6,87 +6,71 @@ use DateTimeInterface; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; +use Tienvx\Bundle\MbtBundle\Entity\Bug\Video; use Tienvx\Bundle\MbtBundle\Model\Bug as BugModel; +use Tienvx\Bundle\MbtBundle\Model\Bug\VideoInterface; use Tienvx\Bundle\MbtBundle\Model\ProgressInterface; use Tienvx\Bundle\MbtBundle\Model\TaskInterface; +use Tienvx\Bundle\MbtBundle\Repository\BugRepository; +use Tienvx\Bundle\MbtBundle\ValueObject\Bug\Step; -/** - * @ORM\Entity - * @ORM\HasLifecycleCallbacks - */ +#[ORM\Entity(repositoryClass: BugRepository::class)] +#[ORM\HasLifecycleCallbacks] class Bug extends BugModel { - /** - * @ORM\Column(type="integer") - * @ORM\Id() - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] protected ?int $id = null; - /** - * @ORM\Column(type="string") - * @Assert\NotBlank - */ + #[ORM\Column(type: 'string')] + #[Assert\NotBlank] protected string $title; - /** - * @ORM\Column(type="array") - * @Assert\All({ - * @Assert\Type("\Tienvx\Bundle\MbtBundle\ValueObject\Bug\Step") - * }) - * @Assert\Valid - */ + #[ORM\Column(type: 'array')] + #[Assert\All([ + new Assert\Type(type: Step::class), + ])] + #[Assert\Valid] protected array $steps = []; - /** - * @ORM\ManyToOne(targetEntity="\Tienvx\Bundle\MbtBundle\Entity\Task", inversedBy="bugs") - * @ORM\JoinColumn(nullable=false) - */ + #[ORM\ManyToOne(targetEntity: Task::class, inversedBy: 'bugs')] + #[ORM\JoinColumn(nullable: false)] protected TaskInterface $task; - /** - * @ORM\Column(type="text") - */ + #[ORM\Column(type: 'text')] protected string $message; - /** - * @ORM\Embedded(class="Progress") - */ + #[ORM\Embedded(class: Progress::class)] protected ProgressInterface $progress; - /** - * @ORM\Column(type="boolean") - */ + #[ORM\Column(type: 'boolean')] protected bool $closed = false; - /** - * @ORM\Column(name="created_at", type="datetime") - */ + #[ORM\Embedded(class: Video::class)] + protected VideoInterface $video; + + #[ORM\Column(name: 'created_at', type: 'datetime')] protected DateTimeInterface $createdAt; - /** - * @ORM\Column(name="updated_at", type="datetime") - */ + #[ORM\Column(name: 'updated_at', type: 'datetime')] protected DateTimeInterface $updatedAt; public function __construct() { parent::__construct(); $this->progress = new Progress(); + $this->video = new Video(); } - /** - * @ORM\PrePersist - */ + #[ORM\PrePersist] public function prePersist(): void { $this->setCreatedAt(new DateTime()); $this->setUpdatedAt(new DateTime()); } - /** - * @ORM\PreUpdate - */ + #[ORM\PreUpdate] public function preUpdate(): void { $this->setUpdatedAt(new DateTime()); diff --git a/src/Entity/Bug/Video.php b/src/Entity/Bug/Video.php new file mode 100644 index 00000000..ad708086 --- /dev/null +++ b/src/Entity/Bug/Video.php @@ -0,0 +1,17 @@ +revisions = new ArrayCollection(); - } - - /** - * @ORM\PrePersist - */ + #[ORM\PrePersist] public function prePersist(): void { $this->setCreatedAt(new DateTime()); $this->setUpdatedAt(new DateTime()); } - /** - * @ORM\PreUpdate - */ + #[ORM\PreUpdate] public function preUpdate(): void { $this->setUpdatedAt(new DateTime()); diff --git a/src/Entity/Model/Revision.php b/src/Entity/Model/Revision.php index 1dec850e..02b9ed80 100644 --- a/src/Entity/Model/Revision.php +++ b/src/Entity/Model/Revision.php @@ -5,60 +5,58 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Tienvx\Bundle\MbtBundle\Entity\Model; use Tienvx\Bundle\MbtBundle\Model\Model\Revision as BaseRevision; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\TransitionInterface; use Tienvx\Bundle\MbtBundle\Model\ModelInterface; +use Tienvx\Bundle\MbtBundle\ValueObject\Model\Place; +use Tienvx\Bundle\MbtBundle\ValueObject\Model\Transition; -/** - * @ORM\Entity - */ +#[ORM\Entity] class Revision extends BaseRevision { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] protected ?int $id = null; - /** - * @ORM\ManyToOne(targetEntity="Tienvx\Bundle\MbtBundle\Entity\Model", inversedBy="revisions") - * @ORM\JoinColumn(nullable=true, onDelete="SET NULL") - */ + #[ORM\ManyToOne(targetEntity: Model::class, inversedBy: 'revisions')] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] protected ?ModelInterface $model = null; - /** - * @ORM\Column(type="array") - * @Assert\All({ - * @Assert\Type("\Tienvx\Bundle\MbtBundle\ValueObject\Model\Place") - * }) - * @Assert\Valid - */ + #[ORM\Column(type: 'array')] + #[Assert\All([ + new Assert\Type(type: Place::class), + ])] + #[Assert\Valid] + #[Assert\Count(min: 1)] protected array $places = []; - /** - * @ORM\Column(type="array") - * @Assert\All({ - * @Assert\Type("\Tienvx\Bundle\MbtBundle\ValueObject\Model\Transition") - * }) - * @Assert\Valid - */ + #[ORM\Column(type: 'array')] + #[Assert\All([ + new Assert\Type(type: Transition::class), + ])] + #[Assert\Valid] + #[Assert\Count(min: 1)] protected array $transitions = []; - /** - * @Assert\Callback - */ + #[Assert\Callback] public function validatePlacesInTransitions(ExecutionContextInterface $context, $payload): void { foreach ($this->transitions as $index => $transition) { if ($transition instanceof TransitionInterface) { $fromPlaces = $transition->getFromPlaces(); + $toPlaces = $transition->getToPlaces(); + if (!$fromPlaces && !$toPlaces) { + $context->buildViolation('mbt.model.missing_places') + ->atPath(sprintf('transitions[%d]', $index)) + ->addViolation(); + } if ($fromPlaces && array_diff($fromPlaces, array_keys($this->places))) { $context->buildViolation('mbt.model.places_invalid') ->atPath(sprintf('transitions[%d].fromPlaces', $index)) ->addViolation(); } - $toPlaces = $transition->getToPlaces(); if ($toPlaces && array_diff($toPlaces, array_keys($this->places))) { $context->buildViolation('mbt.model.places_invalid') ->atPath(sprintf('transitions[%d].toPlaces', $index)) @@ -68,11 +66,13 @@ public function validatePlacesInTransitions(ExecutionContextInterface $context, } } - /** - * @Assert\Callback - */ + #[Assert\Callback] public function validateStartTransitions(ExecutionContextInterface $context, $payload): void { + if (0 === count($this->transitions)) { + return; + } + $startTransitions = array_filter( $this->transitions, fn ($transition) => $transition instanceof TransitionInterface && 0 === count($transition->getFromPlaces()) diff --git a/src/Entity/Progress.php b/src/Entity/Progress.php index b477928d..a0e3cbe7 100644 --- a/src/Entity/Progress.php +++ b/src/Entity/Progress.php @@ -7,26 +7,18 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; use Tienvx\Bundle\MbtBundle\Model\Progress as ProgressModel; -/** - * @ORM\Embeddable - */ +#[ORM\Embeddable] class Progress extends ProgressModel { - /** - * @ORM\Column(type="integer", options={"default": 0}) - * @Assert\Positive - */ + #[ORM\Column(type: 'integer', options: ['default' => 0])] + #[Assert\Positive] protected int $total = 0; - /** - * @ORM\Column(type="integer", options={"default": 0}) - * @Assert\Positive - */ + #[ORM\Column(type: 'integer', options: ['default' => 0])] + #[Assert\Positive] protected int $processed = 0; - /** - * @Assert\Callback - */ + #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload) { if ($this->processed > $this->total) { diff --git a/src/Entity/Task.php b/src/Entity/Task.php index 8dd0d120..21794326 100644 --- a/src/Entity/Task.php +++ b/src/Entity/Task.php @@ -4,96 +4,67 @@ use DateTime; use DateTimeInterface; -use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\Embedded; use Symfony\Component\Validator\Constraints as Assert; +use Tienvx\Bundle\MbtBundle\Entity\Model\Revision; +use Tienvx\Bundle\MbtBundle\Entity\Task\Browser; use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; use Tienvx\Bundle\MbtBundle\Model\Task as TaskModel; use Tienvx\Bundle\MbtBundle\Model\Task\BrowserInterface; +use Tienvx\Bundle\MbtBundle\Repository\TaskRepository; -/** - * @ORM\Entity - * @ORM\HasLifecycleCallbacks - */ +#[ORM\Entity(repositoryClass: TaskRepository::class)] +#[ORM\HasLifecycleCallbacks] class Task extends TaskModel { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] protected ?int $id = null; - /** - * @ORM\Column(type="string") - * @Assert\NotBlank - */ + #[ORM\Column(type: 'string')] + #[Assert\NotBlank] protected string $title = ''; - /** - * @ORM\ManyToOne(targetEntity="\Tienvx\Bundle\MbtBundle\Entity\Model\Revision") - * @ORM\JoinColumn(nullable=false) - * @Assert\Valid - * @Assert\NotNull - */ + #[ORM\ManyToOne(targetEntity: Revision::class)] + #[ORM\JoinColumn(nullable: false)] + #[Assert\Valid] + #[Assert\NotNull] protected RevisionInterface $modelRevision; - /** - * @ORM\Column(type="integer", nullable=true) - */ + #[ORM\Column(type: 'integer', nullable: true)] protected ?int $author = null; - /** - * @ORM\Column(type="boolean") - */ + #[ORM\Column(type: 'boolean')] protected bool $running = false; - /** - * @Embedded(class="\Tienvx\Bundle\MbtBundle\Entity\Task\Browser") - * @Assert\NotNull - */ + #[Embedded(class: Browser::class)] + #[Assert\NotNull] protected BrowserInterface $browser; - /** - * @ORM\OneToMany(targetEntity="\Tienvx\Bundle\MbtBundle\Entity\Bug", mappedBy="task", cascade={"persist"}) - * @ORM\JoinColumn(onDelete="CASCADE") - */ + #[ORM\OneToMany(targetEntity: Bug::class, mappedBy: 'task', cascade: ['persist'])] + #[ORM\JoinColumn(onDelete: 'CASCADE')] protected Collection $bugs; - /** - * @ORM\Column(type="boolean") - */ + #[ORM\Column(type: 'boolean')] protected bool $debug = false; - /** - * @ORM\Column(name="created_at", type="datetime") - */ + #[ORM\Column(name: 'created_at', type: 'datetime')] protected DateTimeInterface $createdAt; - /** - * @ORM\Column(name="updated_at", type="datetime") - */ + #[ORM\Column(name: 'updated_at', type: 'datetime')] protected DateTimeInterface $updatedAt; - public function __construct() - { - $this->bugs = new ArrayCollection(); - } - - /** - * @ORM\PrePersist - */ + #[ORM\PrePersist] public function prePersist(): void { $this->setCreatedAt(new DateTime()); $this->setUpdatedAt(new DateTime()); } - /** - * @ORM\PreUpdate - */ + #[ORM\PreUpdate] public function preUpdate(): void { $this->setUpdatedAt(new DateTime()); diff --git a/src/Entity/Task/Browser.php b/src/Entity/Task/Browser.php index 0ab5c490..db753c46 100644 --- a/src/Entity/Task/Browser.php +++ b/src/Entity/Task/Browser.php @@ -7,20 +7,14 @@ use Symfony\Component\Validator\Constraints as Assert; use Tienvx\Bundle\MbtBundle\Model\Task\Browser as BrowserModel; -/** - * @Embeddable - */ +#[Embeddable] class Browser extends BrowserModel { - /** - * @ORM\Column(type="string", nullable=false) - * @Assert\NotBlank - */ + #[ORM\Column(type: 'string', nullable: false)] + #[Assert\NotBlank] protected string $name = ''; - /** - * @ORM\Column(type="string", nullable=false) - * @Assert\NotBlank - */ + #[ORM\Column(type: 'string', nullable: false)] + #[Assert\NotBlank] protected string $version = ''; } diff --git a/src/EventListener/EntitySubscriber.php b/src/EventListener/EntitySubscriber.php index 0b6ea242..aba160f5 100644 --- a/src/EventListener/EntitySubscriber.php +++ b/src/EventListener/EntitySubscriber.php @@ -11,13 +11,10 @@ class EntitySubscriber implements EventSubscriber { - protected MessageBusInterface $messageBus; - protected string $reducer; - public function __construct(MessageBusInterface $messageBus) + public function __construct(protected MessageBusInterface $messageBus) { - $this->messageBus = $messageBus; } public function postPersist(LifecycleEventArgs $args): void diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 00000000..92570859 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,7 @@ +setLabel($data['label'] ?? ''); $place->setCommands( - array_map([CommandFactory::class, 'createFromArray'], ($data['commands'] ?? [])) + array_map([CommandFactory::class, 'createFromArray'], $data['commands'] ?? []) ); return $place; diff --git a/src/Factory/Model/Revision/TransitionFactory.php b/src/Factory/Model/Revision/TransitionFactory.php index 90eddd7a..704b8224 100644 --- a/src/Factory/Model/Revision/TransitionFactory.php +++ b/src/Factory/Model/Revision/TransitionFactory.php @@ -12,8 +12,9 @@ public static function createFromArray(array $data): TransitionInterface $transition = new Transition(); $transition->setLabel($data['label'] ?? ''); $transition->setGuard($data['guard'] ?? null); + $transition->setExpression($data['expression'] ?? null); $transition->setCommands( - array_map([CommandFactory::class, 'createFromArray'], ($data['commands'] ?? [])) + array_map([CommandFactory::class, 'createFromArray'], $data['commands'] ?? []) ); $transition->setFromPlaces($data['fromPlaces'] ?? []); $transition->setToPlaces($data['toPlaces'] ?? []); diff --git a/src/Generator/GeneratorManager.php b/src/Generator/GeneratorManager.php index 1721ccf5..c56bbe23 100644 --- a/src/Generator/GeneratorManager.php +++ b/src/Generator/GeneratorManager.php @@ -2,17 +2,22 @@ namespace Tienvx\Bundle\MbtBundle\Generator; -use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; -use Tienvx\Bundle\MbtBundle\Plugin\AbstractPluginManager; +use Tienvx\Bundle\MbtBundle\Plugin\PluginManager; -class GeneratorManager extends AbstractPluginManager implements GeneratorManagerInterface +class GeneratorManager extends PluginManager implements GeneratorManagerInterface { public function getGenerator(string $name): GeneratorInterface { - if ($this->has($name) && ($generator = $this->get($name)) && $generator instanceof GeneratorInterface) { - return $generator; - } + return parent::get($name); + } - throw new UnexpectedValueException(sprintf('Generator "%s" does not exist.', $name)); + protected function getPluginInterface(): string + { + return GeneratorInterface::class; + } + + protected function getInvalidPluginExceptionMessage(string $name): string + { + return sprintf('Generator "%s" does not exist.', $name); } } diff --git a/src/Generator/RandomGenerator.php b/src/Generator/RandomGenerator.php index 49e3735a..49534fd9 100644 --- a/src/Generator/RandomGenerator.php +++ b/src/Generator/RandomGenerator.php @@ -3,14 +3,15 @@ namespace Tienvx\Bundle\MbtBundle\Generator; use Petrinet\Model\MarkingInterface; -use Petrinet\Model\PetrinetInterface; use Petrinet\Model\TransitionInterface; +use SingleColorPetrinet\Model\PetrinetInterface; use SingleColorPetrinet\Service\GuardedTransitionServiceInterface; use Tienvx\Bundle\MbtBundle\Model\Generator\State; use Tienvx\Bundle\MbtBundle\Model\Generator\StateInterface; use Tienvx\Bundle\MbtBundle\Model\TaskInterface; use Tienvx\Bundle\MbtBundle\Service\Model\ModelHelperInterface; use Tienvx\Bundle\MbtBundle\Service\Petrinet\MarkingHelperInterface; +use Tienvx\Bundle\MbtBundle\Service\Petrinet\PetrinetHelper; use Tienvx\Bundle\MbtBundle\Service\Petrinet\PetrinetHelperInterface; use Tienvx\Bundle\MbtBundle\ValueObject\Bug\Step; @@ -19,21 +20,12 @@ class RandomGenerator extends AbstractGenerator public const MAX_TRANSITION_COVERAGE = 100; public const MAX_PLACE_COVERAGE = 100; - protected PetrinetHelperInterface $petrinetHelper; - protected MarkingHelperInterface $markingHelper; - protected ModelHelperInterface $modelHelper; - protected GuardedTransitionServiceInterface $transitionService; - public function __construct( - PetrinetHelperInterface $petrinetHelper, - MarkingHelperInterface $markingHelper, - ModelHelperInterface $modelHelper, - GuardedTransitionServiceInterface $transitionService + protected PetrinetHelperInterface $petrinetHelper, + protected MarkingHelperInterface $markingHelper, + protected ModelHelperInterface $modelHelper, + protected GuardedTransitionServiceInterface $transitionService ) { - $this->petrinetHelper = $petrinetHelper; - $this->markingHelper = $markingHelper; - $this->modelHelper = $modelHelper; - $this->transitionService = $transitionService; } public static function getName(): string @@ -44,35 +36,31 @@ public static function getName(): string public function generate(TaskInterface $task): iterable { $petrinet = $this->petrinetHelper->build($task->getModelRevision()); - $transition = null; - $transitionId = $this->modelHelper->getStartTransitionId($task->getModelRevision()); - $places = $this->modelHelper->getStartPlaceIds($task->getModelRevision()); - $marking = $this->markingHelper->getMarking($petrinet, $places); + $transition = $petrinet->getTransitionById( + $this->modelHelper->getStartTransitionId($task->getModelRevision()) + ); + $marking = $this->markingHelper->getMarking($petrinet, [PetrinetHelper::FAKE_START_PLACE_ID => 1]); $state = new State( - [$transitionId], + [], + [], count($task->getModelRevision()->getPlaces()), count($task->getModelRevision()->getTransitions()) ); while ($this->canContinue($state)) { - if ($transition) { - $this->transitionService->fire($transition, $marking); - $places = $this->markingHelper->getPlaces($marking); - } - $this->update($state, $marking, $transitionId); + $this->transitionService->fire($transition, $marking); + $this->update($state, $marking, $transition->getId()); yield new Step( - $places, + $this->markingHelper->getPlaces($marking), $marking->getColor(), - $transitionId + $transition->getId() ); $transition = $this->nextTransition($petrinet, $marking); if (!$transition) { break; } - - $transitionId = $transition->getId(); } } @@ -93,10 +81,6 @@ protected function update(StateInterface $state, MarkingInterface $marking, int } } $state->addVisitedTransition($transitionId); - - // Update current coverage. - $state->setTransitionCoverage(count($state->getVisitedTransitions()) / $state->getTotalTransitions() * 100); - $state->setPlaceCoverage(count($state->getVisitedPlaces()) / $state->getTotalPlaces() * 100); } protected function nextTransition(PetrinetInterface $petrinet, MarkingInterface $marking): ?TransitionInterface diff --git a/src/Message/CreateBugMessage.php b/src/Message/CreateBugMessage.php new file mode 100644 index 00000000..4d6bf4e7 --- /dev/null +++ b/src/Message/CreateBugMessage.php @@ -0,0 +1,28 @@ +taskId; + } + + public function getSteps(): array + { + return $this->steps; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/Message/RecordVideoMessage.php b/src/Message/RecordVideoMessage.php index 9316b80f..11b10275 100644 --- a/src/Message/RecordVideoMessage.php +++ b/src/Message/RecordVideoMessage.php @@ -4,11 +4,8 @@ class RecordVideoMessage implements MessageInterface { - protected int $bugId; - - public function __construct(int $bugId) + public function __construct(protected int $bugId) { - $this->bugId = $bugId; } public function getBugId(): int diff --git a/src/Message/ReduceBugMessage.php b/src/Message/ReduceBugMessage.php index d802a850..a9253eb9 100644 --- a/src/Message/ReduceBugMessage.php +++ b/src/Message/ReduceBugMessage.php @@ -4,11 +4,8 @@ class ReduceBugMessage implements MessageInterface { - protected int $id; - - public function __construct(int $id) + public function __construct(protected int $id) { - $this->id = $id; } public function getId(): int diff --git a/src/Message/ReduceStepsMessage.php b/src/Message/ReduceStepsMessage.php index 110051b2..12f8e636 100644 --- a/src/Message/ReduceStepsMessage.php +++ b/src/Message/ReduceStepsMessage.php @@ -4,17 +4,12 @@ class ReduceStepsMessage implements MessageInterface { - protected int $bugId; - protected int $length; - protected int $from; - protected int $to; - - public function __construct(int $bugId, int $length, int $from, int $to) - { - $this->bugId = $bugId; - $this->length = $length; - $this->from = $from; - $this->to = $to; + public function __construct( + protected int $bugId, + protected int $length, + protected int $from, + protected int $to + ) { } public function getBugId(): int diff --git a/src/Message/ReportBugMessage.php b/src/Message/ReportBugMessage.php index 05795e8c..090e915f 100644 --- a/src/Message/ReportBugMessage.php +++ b/src/Message/ReportBugMessage.php @@ -4,11 +4,8 @@ class ReportBugMessage implements MessageInterface { - protected int $bugId; - - public function __construct(int $bugId) + public function __construct(protected int $bugId) { - $this->bugId = $bugId; } public function getBugId(): int diff --git a/src/Message/RunTaskMessage.php b/src/Message/RunTaskMessage.php index 2110d432..dd7878b5 100644 --- a/src/Message/RunTaskMessage.php +++ b/src/Message/RunTaskMessage.php @@ -4,11 +4,8 @@ class RunTaskMessage implements MessageInterface { - protected int $id; - - public function __construct(int $id) + public function __construct(protected int $id) { - $this->id = $id; } public function getId(): int diff --git a/src/MessageHandler/CreateBugMessageHandler.php b/src/MessageHandler/CreateBugMessageHandler.php new file mode 100644 index 00000000..bba7da52 --- /dev/null +++ b/src/MessageHandler/CreateBugMessageHandler.php @@ -0,0 +1,45 @@ +taskRepository->find($message->getTaskId()); + + if (!$task instanceof TaskInterface) { + throw new UnexpectedValueException(sprintf( + 'Can not create bug for task %d: task not found', + $message->getTaskId() + )); + } + + $this->taskRepository->addBug($task, $this->createBug($message->getSteps(), $message->getMessage())); + } + + public function createBug(array $steps, string $message): BugInterface + { + $bug = new Bug(); + $bug->setTitle($this->config->getDefaultBugTitle()); + $bug->setSteps($steps); + $bug->setMessage($message); + + return $bug; + } +} diff --git a/src/MessageHandler/RecordVideoMessageHandler.php b/src/MessageHandler/RecordVideoMessageHandler.php index 36deb367..b360b651 100644 --- a/src/MessageHandler/RecordVideoMessageHandler.php +++ b/src/MessageHandler/RecordVideoMessageHandler.php @@ -8,11 +8,8 @@ class RecordVideoMessageHandler implements MessageHandlerInterface { - protected BugHelperInterface $bugHelper; - - public function __construct(BugHelperInterface $bugHelper) + public function __construct(protected BugHelperInterface $bugHelper) { - $this->bugHelper = $bugHelper; } public function __invoke(RecordVideoMessage $message): void diff --git a/src/MessageHandler/ReduceBugMessageHandler.php b/src/MessageHandler/ReduceBugMessageHandler.php index 937469d4..42010be1 100644 --- a/src/MessageHandler/ReduceBugMessageHandler.php +++ b/src/MessageHandler/ReduceBugMessageHandler.php @@ -8,11 +8,8 @@ class ReduceBugMessageHandler implements MessageHandlerInterface { - protected BugHelperInterface $bugHelper; - - public function __construct(BugHelperInterface $bugHelper) + public function __construct(protected BugHelperInterface $bugHelper) { - $this->bugHelper = $bugHelper; } public function __invoke(ReduceBugMessage $message): void diff --git a/src/MessageHandler/ReduceStepsMessageHandler.php b/src/MessageHandler/ReduceStepsMessageHandler.php index 61cfd587..a6ffd4d3 100644 --- a/src/MessageHandler/ReduceStepsMessageHandler.php +++ b/src/MessageHandler/ReduceStepsMessageHandler.php @@ -8,11 +8,8 @@ class ReduceStepsMessageHandler implements MessageHandlerInterface { - protected BugHelperInterface $bugHelper; - - public function __construct(BugHelperInterface $bugHelper) + public function __construct(protected BugHelperInterface $bugHelper) { - $this->bugHelper = $bugHelper; } public function __invoke(ReduceStepsMessage $message): void diff --git a/src/MessageHandler/ReportBugMessageHandler.php b/src/MessageHandler/ReportBugMessageHandler.php index 9bd67b9a..28047960 100644 --- a/src/MessageHandler/ReportBugMessageHandler.php +++ b/src/MessageHandler/ReportBugMessageHandler.php @@ -8,11 +8,8 @@ class ReportBugMessageHandler implements MessageHandlerInterface { - protected BugHelperInterface $bugHelper; - - public function __construct(BugHelperInterface $bugHelper) + public function __construct(protected BugHelperInterface $bugHelper) { - $this->bugHelper = $bugHelper; } public function __invoke(ReportBugMessage $message): void diff --git a/src/MessageHandler/RunTaskMessageHandler.php b/src/MessageHandler/RunTaskMessageHandler.php index f35b2102..59668e6e 100644 --- a/src/MessageHandler/RunTaskMessageHandler.php +++ b/src/MessageHandler/RunTaskMessageHandler.php @@ -8,11 +8,8 @@ class RunTaskMessageHandler implements MessageHandlerInterface { - protected TaskHelperInterface $taskHelper; - - public function __construct(TaskHelperInterface $taskHelper) + public function __construct(protected TaskHelperInterface $taskHelper) { - $this->taskHelper = $taskHelper; } public function __invoke(RunTaskMessage $message): void diff --git a/src/Model/Bug.php b/src/Model/Bug.php index 20a996d7..c57e13c9 100644 --- a/src/Model/Bug.php +++ b/src/Model/Bug.php @@ -4,9 +4,13 @@ use DateTimeInterface; use Tienvx\Bundle\MbtBundle\Model\Bug\StepInterface; +use Tienvx\Bundle\MbtBundle\Model\Bug\Video; +use Tienvx\Bundle\MbtBundle\Model\Bug\VideoInterface; -abstract class Bug implements BugInterface +class Bug extends Debug implements BugInterface { + protected const BUG = 'bug'; + protected ?int $id = null; protected string $title; protected array $steps = []; @@ -14,12 +18,14 @@ abstract class Bug implements BugInterface protected string $message; protected ProgressInterface $progress; protected bool $closed = false; + protected VideoInterface $video; protected DateTimeInterface $updatedAt; protected DateTimeInterface $createdAt; public function __construct() { $this->progress = new Progress(); + $this->video = new Video(); } public function setId(int $id) @@ -100,6 +106,26 @@ public function setClosed(bool $closed): void $this->closed = $closed; } + public function getVideo(): VideoInterface + { + return $this->video; + } + + public function setVideo(VideoInterface $video): void + { + $this->video = $video; + } + + public function getLogName(): string + { + return sprintf('%s-%d.log', static::BUG, $this->getId()); + } + + public function getVideoName(): string + { + return sprintf('%s-%d.mp4', static::BUG, $this->getId()); + } + public function setCreatedAt(DateTimeInterface $createdAt): void { $this->createdAt = $createdAt; diff --git a/src/Model/Bug/Step.php b/src/Model/Bug/Step.php index ea95112f..d966da01 100644 --- a/src/Model/Bug/Step.php +++ b/src/Model/Bug/Step.php @@ -8,10 +8,6 @@ class Step implements StepInterface, NodeIdentifierInterface { - protected ColorInterface $color; - protected array $places; - protected int $transition; - public function __serialize(): array { return [ @@ -28,11 +24,12 @@ public function __unserialize(array $data) $this->transition = $data['transition']; } - public function __construct(array $places, ColorInterface $color, int $transition) - { + public function __construct( + protected array $places, + protected ColorInterface $color, + protected int $transition + ) { $this->setPlaces($places); - $this->setColor($color); - $this->setTransition($transition); } public function __clone() @@ -42,15 +39,9 @@ public function __clone() public function getUniqueNodeId(): string { - $places = $this->places; - ksort($places); - $colorValues = $this->color->getValues(); - ksort($colorValues); - - return md5(serialize([ - 'places' => $places, - 'color' => $colorValues, - ])); + ksort($this->places); + + return md5(serialize($this->places)); } public function setColor(ColorInterface $color): void diff --git a/src/Model/Bug/Video.php b/src/Model/Bug/Video.php new file mode 100644 index 00000000..46a0c9fd --- /dev/null +++ b/src/Model/Bug/Video.php @@ -0,0 +1,29 @@ +recording; + } + + public function setRecording(bool $recording): void + { + $this->recording = $recording; + } + + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } + + public function setErrorMessage(?string $errorMessage = null): void + { + $this->errorMessage = $errorMessage; + } +} diff --git a/src/Model/Bug/VideoInterface.php b/src/Model/Bug/VideoInterface.php new file mode 100644 index 00000000..48c1a75d --- /dev/null +++ b/src/Model/Bug/VideoInterface.php @@ -0,0 +1,14 @@ +debug; + } + + public function setDebug(bool $debug): void + { + $this->debug = $debug; + } +} diff --git a/src/Model/DebugInterface.php b/src/Model/DebugInterface.php new file mode 100644 index 00000000..b8518acc --- /dev/null +++ b/src/Model/DebugInterface.php @@ -0,0 +1,16 @@ +visitedTransitions = $visitedTransitions; - $this->totalPlaces = $totalPlaces; - $this->totalTransitions = $totalTransitions; - } - - public function getVisitedPlaces(): array - { - return $this->visitedPlaces; - } - - public function setVisitedPlaces(array $visitedPlaces): void - { - $this->visitedPlaces = []; - - foreach ($visitedPlaces as $visitedPlace) { - $this->addVisitedPlace($visitedPlace); + if ($totalPlaces <= 0 || $totalTransitions <= 0) { + throw new OutOfRangeException('State need at least 1 place and 1 transition'); } + $this->updateCoverage(); } public function addVisitedPlace(int $placeId) { if (!in_array($placeId, $this->visitedPlaces)) { $this->visitedPlaces[] = $placeId; - } - } - - public function getTotalPlaces(): int - { - return $this->totalPlaces; - } - - public function setTotalPlaces(int $totalPlaces): void - { - $this->totalPlaces = $totalPlaces; - } - - public function getVisitedTransitions(): array - { - return $this->visitedTransitions; - } - - public function setVisitedTransitions(array $visitedTransitions): void - { - $this->visitedTransitions = []; - - foreach ($visitedTransitions as $visitedTransition) { - $this->addVisitedTransition($visitedTransition); + $this->updateCoverage(); } } @@ -70,36 +33,23 @@ public function addVisitedTransition(int $transitionId) { if (!in_array($transitionId, $this->visitedTransitions)) { $this->visitedTransitions[] = $transitionId; + $this->updateCoverage(); } } - public function getTotalTransitions(): int - { - return $this->totalTransitions; - } - - public function setTotalTransitions(int $totalTransitions): void - { - $this->totalTransitions = $totalTransitions; - } - public function getTransitionCoverage(): float { return $this->transitionCoverage; } - public function setTransitionCoverage(float $transitionCoverage): void - { - $this->transitionCoverage = $transitionCoverage; - } - public function getPlaceCoverage(): float { return $this->placeCoverage; } - public function setPlaceCoverage(float $placeCoverage): void + protected function updateCoverage(): void { - $this->placeCoverage = $placeCoverage; + $this->transitionCoverage = count($this->visitedTransitions) / $this->totalTransitions * 100; + $this->placeCoverage = count($this->visitedPlaces) / $this->totalPlaces * 100; } } diff --git a/src/Model/Generator/StateInterface.php b/src/Model/Generator/StateInterface.php index 01bb9913..997cb988 100644 --- a/src/Model/Generator/StateInterface.php +++ b/src/Model/Generator/StateInterface.php @@ -4,31 +4,11 @@ interface StateInterface { - public function getVisitedPlaces(): array; - - public function setVisitedPlaces(array $visitedPlaces): void; - public function addVisitedPlace(int $placeId); - public function getTotalPlaces(): int; - - public function setTotalPlaces(int $totalPlaces): void; - - public function getVisitedTransitions(): array; - - public function setVisitedTransitions(array $visitedTransitions): void; - public function addVisitedTransition(int $transitionId); - public function getTotalTransitions(): int; - - public function setTotalTransitions(int $totalTransitions): void; - public function getTransitionCoverage(): float; - public function setTransitionCoverage(float $transitionCoverage): void; - public function getPlaceCoverage(): float; - - public function setPlaceCoverage(float $placeCoverage): void; } diff --git a/src/Model/Model.php b/src/Model/Model.php index 1577b46e..53e15f93 100644 --- a/src/Model/Model.php +++ b/src/Model/Model.php @@ -3,10 +3,11 @@ namespace Tienvx\Bundle\MbtBundle\Model; use DateTimeInterface; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; -abstract class Model implements ModelInterface +class Model implements ModelInterface { protected ?int $id = null; protected ?int $author = null; @@ -17,6 +18,11 @@ abstract class Model implements ModelInterface protected DateTimeInterface $updatedAt; protected DateTimeInterface $createdAt; + public function __construct() + { + $this->revisions = new ArrayCollection(); + } + public function setId(int $id) { $this->id = $id; @@ -58,7 +64,7 @@ public function setTags(?string $tags): void } /** - * @return Collection|RevisionInterface[] + * @return Collection */ public function getRevisions(): Collection { diff --git a/src/Model/Model/Revision/Transition.php b/src/Model/Model/Revision/Transition.php index 4f94f15c..22063240 100644 --- a/src/Model/Model/Revision/Transition.php +++ b/src/Model/Model/Revision/Transition.php @@ -11,6 +11,7 @@ class Transition implements TransitionInterface protected string $label = ''; protected ?string $guard = null; + protected ?string $expression = null; protected array $fromPlaces = []; protected array $toPlaces = []; @@ -23,6 +24,7 @@ public function __unserialize(array $data) { $this->label = $data['label']; $this->guard = $data['guard']; + $this->expression = $data['expression']; $this->fromPlaces = $data['fromPlaces']; $this->toPlaces = $data['toPlaces']; $this->commands = array_map([CommandFactory::class, 'createFromArray'], $data['commands']); @@ -48,6 +50,16 @@ public function setGuard(?string $guard): void $this->guard = $guard; } + public function getExpression(): ?string + { + return $this->expression; + } + + public function setExpression(?string $expression): void + { + $this->expression = $expression; + } + public function getFromPlaces(): array { return $this->fromPlaces; @@ -91,9 +103,15 @@ public function toArray(): array return [ 'label' => $this->label, 'guard' => $this->guard, + 'expression' => $this->expression, 'fromPlaces' => $this->fromPlaces, 'toPlaces' => $this->toPlaces, 'commands' => array_map(fn (CommandInterface $command) => $command->toArray(), $this->commands), ]; } + + public function isStart(): bool + { + return empty($this->fromPlaces); + } } diff --git a/src/Model/Model/Revision/TransitionInterface.php b/src/Model/Model/Revision/TransitionInterface.php index 0191cf25..49559ca7 100644 --- a/src/Model/Model/Revision/TransitionInterface.php +++ b/src/Model/Model/Revision/TransitionInterface.php @@ -12,6 +12,10 @@ public function getGuard(): ?string; public function setGuard(?string $guard): void; + public function getExpression(): ?string; + + public function setExpression(?string $expression): void; + public function getCommands(): array; public function setCommands(array $commands): void; @@ -31,4 +35,6 @@ public function setToPlaces(array $toPlaces): void; public function addToPlace(int $toPlace): void; public function toArray(): array; + + public function isStart(): bool; } diff --git a/src/Model/Task.php b/src/Model/Task.php index 85b2805e..0127b30c 100644 --- a/src/Model/Task.php +++ b/src/Model/Task.php @@ -3,12 +3,15 @@ namespace Tienvx\Bundle\MbtBundle\Model; use DateTimeInterface; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; use Tienvx\Bundle\MbtBundle\Model\Task\BrowserInterface; -class Task implements TaskInterface +class Task extends Debug implements TaskInterface { + protected const TASK = 'task'; + protected ?int $id = null; protected string $title = ''; protected RevisionInterface $modelRevision; @@ -16,10 +19,14 @@ class Task implements TaskInterface protected bool $running = false; protected BrowserInterface $browser; protected Collection $bugs; - protected bool $debug = false; protected DateTimeInterface $updatedAt; protected DateTimeInterface $createdAt; + public function __construct() + { + $this->bugs = new ArrayCollection(); + } + public function setId(int $id) { $this->id = $id; @@ -81,7 +88,7 @@ public function setBrowser(BrowserInterface $browser): void } /** - * @return Collection|BugInterface[] + * @return Collection */ public function getBugs(): Collection { @@ -96,14 +103,19 @@ public function addBug(BugInterface $bug): void } } - public function isDebug(): bool + public function getLogName(): string + { + return sprintf('%s-%d.log', static::TASK, $this->getId()); + } + + public function getVideoName(): string { - return $this->debug; + return sprintf('%s-%d.mp4', static::TASK, $this->getId()); } - public function setDebug(bool $debug): void + public function getTask(): TaskInterface { - $this->debug = $debug; + return $this; } public function setCreatedAt(DateTimeInterface $createdAt): void diff --git a/src/Model/TaskInterface.php b/src/Model/TaskInterface.php index 06c2a59a..145f799f 100644 --- a/src/Model/TaskInterface.php +++ b/src/Model/TaskInterface.php @@ -7,7 +7,7 @@ use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; use Tienvx\Bundle\MbtBundle\Model\Task\BrowserInterface; -interface TaskInterface +interface TaskInterface extends DebugInterface { public function setId(int $id); @@ -37,10 +37,6 @@ public function getBugs(): Collection; public function addBug(BugInterface $bug): void; - public function isDebug(): bool; - - public function setDebug(bool $debug): void; - public function setCreatedAt(DateTimeInterface $createdAt): void; public function getCreatedAt(): DateTimeInterface; diff --git a/src/Model/Values.php b/src/Model/Values.php new file mode 100644 index 00000000..59d5460e --- /dev/null +++ b/src/Model/Values.php @@ -0,0 +1,30 @@ +values = []; + + foreach ($values as $key => $value) { + $this->setValue($key, $value); + } + } + + public function getValues(): array + { + return $this->values; + } + + public function setValue(string $key, mixed $value): void + { + $this->values[$key] = $value; + } + + public function getValue(string $key): mixed + { + return $this->values[$key] ?? null; + } +} diff --git a/src/Model/ValuesInterface.php b/src/Model/ValuesInterface.php new file mode 100644 index 00000000..ad05d826 --- /dev/null +++ b/src/Model/ValuesInterface.php @@ -0,0 +1,12 @@ +locator = $locator; - $this->plugins = $plugins; - } - - public function all(): array - { - return $this->plugins; - } - - public function has(string $name): bool - { - return in_array($name, $this->plugins) && $this->locator->has($name); - } - - public function get(string $name): PluginInterface - { - $plugin = $this->locator->has($name) ? $this->locator->get($name) : null; - if ($plugin instanceof PluginInterface) { - return $plugin; - } - - throw new UnexpectedValueException(sprintf('Plugin "%s" does not exist.', $name)); - } -} diff --git a/src/Plugin/PluginManager.php b/src/Plugin/PluginManager.php new file mode 100644 index 00000000..87ba558c --- /dev/null +++ b/src/Plugin/PluginManager.php @@ -0,0 +1,45 @@ +plugins; + } + + public function has(string $name): bool + { + return in_array($name, $this->plugins) && $this->locator->has($name); + } + + public function get(string $name): PluginInterface + { + $plugin = $this->has($name) ? $this->locator->get($name) : null; + if (is_a($plugin, $this->getPluginInterface())) { + return $plugin; + } + + throw new UnexpectedValueException($this->getInvalidPluginExceptionMessage($name)); + } + + protected function getInvalidPluginExceptionMessage(string $name): string + { + return sprintf('Plugin "%s" does not exist.', $name); + } + + protected function getPluginInterface(): string + { + return PluginInterface::class; + } +} diff --git a/src/Reducer/DispatcherTemplate.php b/src/Reducer/DispatcherTemplate.php index af06792f..593286b0 100644 --- a/src/Reducer/DispatcherTemplate.php +++ b/src/Reducer/DispatcherTemplate.php @@ -8,18 +8,17 @@ abstract class DispatcherTemplate implements DispatcherInterface { - protected MessageBusInterface $messageBus; + protected const MIN_STEPS = 3; - public function __construct(MessageBusInterface $messageBus) + public function __construct(protected MessageBusInterface $messageBus) { - $this->messageBus = $messageBus; } public function dispatch(BugInterface $bug): int { $steps = $bug->getSteps(); - if (count($steps) <= 2) { + if (count($steps) < self::MIN_STEPS) { return 0; } @@ -33,10 +32,7 @@ public function dispatch(BugInterface $bug): int return count($pairs); } - protected function getPairs(array $steps): array - { - return []; - } + abstract protected function getPairs(array $steps): array; protected function maxPairs(array $steps): int { diff --git a/src/Reducer/HandlerTemplate.php b/src/Reducer/HandlerTemplate.php index ca799e1d..4b228b81 100644 --- a/src/Reducer/HandlerTemplate.php +++ b/src/Reducer/HandlerTemplate.php @@ -2,92 +2,56 @@ namespace Tienvx\Bundle\MbtBundle\Reducer; -use Doctrine\DBAL\LockMode; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Messenger\MessageBusInterface; use Throwable; -use Tienvx\Bundle\MbtBundle\Exception\ExceptionInterface; +use Tienvx\Bundle\MbtBundle\Exception\StepsNotConnectedException; +use Tienvx\Bundle\MbtBundle\Message\CreateBugMessage; use Tienvx\Bundle\MbtBundle\Message\ReduceBugMessage; use Tienvx\Bundle\MbtBundle\Model\BugInterface; -use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelperInterface; -use Tienvx\Bundle\MbtBundle\Service\SelenoidHelperInterface; -use Tienvx\Bundle\MbtBundle\Service\StepRunnerInterface; -use Tienvx\Bundle\MbtBundle\Service\StepsBuilderInterface; +use Tienvx\Bundle\MbtBundle\Repository\BugRepositoryInterface; +use Tienvx\Bundle\MbtBundle\Service\ConfigInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Builder\StepsBuilderInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Runner\BugStepsRunner; abstract class HandlerTemplate implements HandlerInterface { - protected EntityManagerInterface $entityManager; - protected MessageBusInterface $messageBus; - protected StepRunnerInterface $stepRunner; - protected StepsBuilderInterface $stepsBuilder; - protected BugHelperInterface $bugHelper; - protected SelenoidHelperInterface $selenoidHelper; - public function __construct( - EntityManagerInterface $entityManager, - MessageBusInterface $messageBus, - StepRunnerInterface $stepRunner, - StepsBuilderInterface $stepsBuilder, - BugHelperInterface $bugHelper, - SelenoidHelperInterface $selenoidHelper + protected BugRepositoryInterface $bugRepository, + protected MessageBusInterface $messageBus, + protected BugStepsRunner $stepsRunner, + protected StepsBuilderInterface $stepsBuilder, + protected ConfigInterface $config ) { - $this->entityManager = $entityManager; - $this->messageBus = $messageBus; - $this->stepRunner = $stepRunner; - $this->stepsBuilder = $stepsBuilder; - $this->bugHelper = $bugHelper; - $this->selenoidHelper = $selenoidHelper; } - /** - * @throws ExceptionInterface - */ public function handle(BugInterface $bug, int $from, int $to): void { - $newSteps = iterator_to_array($this->stepsBuilder->create($bug, $from, $to)); - if (count($newSteps) >= count($bug->getSteps())) { + try { + $newSteps = iterator_to_array($this->stepsBuilder->create($bug, $from, $to)); + } catch (StepsNotConnectedException $exception) { return; } - $this->run($newSteps, $bug); - } - - /** - * @throws ExceptionInterface - */ - protected function run(array $newSteps, BugInterface $bug): void - { - $driver = $this->selenoidHelper->createDriver($this->selenoidHelper->getCapabilities($bug)); - try { - foreach ($newSteps as $step) { - $this->stepRunner->run($step, $bug->getTask()->getModelRevision(), $driver); - } - } catch (ExceptionInterface $exception) { - throw $exception; - } catch (Throwable $throwable) { - if ($throwable->getMessage() === $bug->getMessage()) { - $this->updateSteps($bug, $newSteps); - $this->messageBus->dispatch(new ReduceBugMessage($bug->getId())); - } - } finally { - $driver->quit(); + if (count($newSteps) >= count($bug->getSteps())) { + return; } - } - public function updateSteps(BugInterface $bug, array $newSteps): void - { - $callback = function () use ($bug, $newSteps): void { - // Refresh the bug for the latest steps's length. - $this->entityManager->refresh($bug); - - if (count($newSteps) <= count($bug->getSteps())) { - $this->entityManager->lock($bug, LockMode::PESSIMISTIC_WRITE); - $bug->getProgress()->setTotal(0); - $bug->getProgress()->setProcessed(0); - $bug->setSteps($newSteps); + $bug->setDebug(false); + $this->stepsRunner->run( + $newSteps, + $bug, + function (Throwable $throwable) use ($bug, $newSteps): void { + if ($throwable->getMessage() === $bug->getMessage()) { + $this->bugRepository->updateSteps($bug, $newSteps); + $this->messageBus->dispatch(new ReduceBugMessage($bug->getId())); + } elseif ($this->config->shouldCreateNewBugWhileReducing()) { + $this->messageBus->dispatch(new CreateBugMessage( + $bug->getTask()->getId(), + $newSteps, + $throwable->getMessage() + )); + } } - }; - - $this->entityManager->transactional($callback); + ); } } diff --git a/src/Reducer/ReducerManager.php b/src/Reducer/ReducerManager.php index 26b80ef8..5957d1a9 100644 --- a/src/Reducer/ReducerManager.php +++ b/src/Reducer/ReducerManager.php @@ -2,17 +2,22 @@ namespace Tienvx\Bundle\MbtBundle\Reducer; -use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; -use Tienvx\Bundle\MbtBundle\Plugin\AbstractPluginManager; +use Tienvx\Bundle\MbtBundle\Plugin\PluginManager; -class ReducerManager extends AbstractPluginManager implements ReducerManagerInterface +class ReducerManager extends PluginManager implements ReducerManagerInterface { public function getReducer(string $name): ReducerInterface { - if ($this->has($name) && ($reducer = $this->get($name)) && $reducer instanceof ReducerInterface) { - return $reducer; - } + return $this->get($name); + } - throw new UnexpectedValueException(sprintf('Reducer "%s" does not exist.', $name)); + protected function getPluginInterface(): string + { + return ReducerInterface::class; + } + + protected function getInvalidPluginExceptionMessage(string $name): string + { + return sprintf('Reducer "%s" does not exist.', $name); } } diff --git a/src/Reducer/Split/SplitDispatcher.php b/src/Reducer/Split/SplitDispatcher.php index c88029ad..8729e0b3 100644 --- a/src/Reducer/Split/SplitDispatcher.php +++ b/src/Reducer/Split/SplitDispatcher.php @@ -12,7 +12,7 @@ protected function getPairs(array $steps): array $maxPairs = $this->maxPairs($steps); $pairs = []; - $range = range(0, $length - 1, ceil($length / $maxPairs)); + $range = range(0, $length - 1, (int) ceil($length / $maxPairs)); if (end($range) !== $length - 1) { $range[] = $length - 1; } diff --git a/src/Repository/BugRepository.php b/src/Repository/BugRepository.php new file mode 100644 index 00000000..bd1ae320 --- /dev/null +++ b/src/Repository/BugRepository.php @@ -0,0 +1,69 @@ +getEntityManager()->wrapInTransaction(function () use ($bug, $newSteps): void { + // Refresh the bug for the latest steps's length. + $this->getEntityManager()->refresh($bug); + + if (count($newSteps) < count($bug->getSteps())) { + $this->getEntityManager()->lock($bug, LockMode::PESSIMISTIC_WRITE); + $bug->getProgress()->setTotal(0); + $bug->getProgress()->setProcessed(0); + $bug->setSteps($newSteps); + } + }); + } + + public function increaseProcessed(BugInterface $bug, int $processed = 1): void + { + $this->getEntityManager()->wrapInTransaction(function () use ($bug, $processed): void { + // Refresh the bug for the latest progress. + $this->getEntityManager()->refresh($bug); + + $this->getEntityManager()->lock($bug, LockMode::PESSIMISTIC_WRITE); + $bug->getProgress()->increase($processed); + }); + } + + public function increaseTotal(BugInterface $bug, int $total): void + { + $this->getEntityManager()->wrapInTransaction(function () use ($bug, $total): void { + // Refresh the bug for the latest progress. + $this->getEntityManager()->refresh($bug); + + $this->getEntityManager()->lock($bug, LockMode::PESSIMISTIC_WRITE); + $bug->getProgress()->setTotal($bug->getProgress()->getTotal() + $total); + }); + } + + public function startRecording(BugInterface $bug): void + { + $this->getEntityManager()->refresh($bug); + $bug->getVideo()->setRecording(true); + $this->getEntityManager()->flush(); + } + + public function stopRecording(BugInterface $bug): void + { + // Recording bug may take long time. Reconnect to flush changes. + $this->getEntityManager()->getConnection()->connect(); + $bug->getVideo()->setRecording(false); + $this->getEntityManager()->flush(); + } +} diff --git a/src/Repository/BugRepositoryInterface.php b/src/Repository/BugRepositoryInterface.php new file mode 100644 index 00000000..f834c7c8 --- /dev/null +++ b/src/Repository/BugRepositoryInterface.php @@ -0,0 +1,19 @@ +setRunning(true); + $this->getEntityManager()->flush(); + } + + public function stopRunning(TaskInterface $task): void + { + $task->setRunning(false); + // Running task take long time. Reconnect to flush changes. + $this->getEntityManager()->getConnection()->connect(); + $this->getEntityManager()->flush(); + } + + public function addBug(TaskInterface $task, BugInterface $bug): void + { + $task->addBug($bug); + $this->getEntityManager()->flush(); + } +} diff --git a/src/Repository/TaskRepositoryInterface.php b/src/Repository/TaskRepositoryInterface.php new file mode 100644 index 00000000..fe51d651 --- /dev/null +++ b/src/Repository/TaskRepositoryInterface.php @@ -0,0 +1,16 @@ +reducerManager = $reducerManager; - $this->entityManager = $entityManager; - $this->messageBus = $messageBus; - $this->bugProgress = $bugProgress; - $this->notifyHelper = $notifyHelper; - $this->stepRunner = $stepRunner; - $this->selenoidHelper = $selenoidHelper; - $this->config = $config; } public function reduceBug(int $bugId): void @@ -54,7 +34,7 @@ public function reduceBug(int $bugId): void $reducer = $this->reducerManager->getReducer($this->config->getReducer()); $messagesCount = $reducer->dispatch($bug); if ($messagesCount > 0) { - $this->bugProgress->increaseTotal($bug, $messagesCount); + $this->bugRepository->increaseTotal($bug, $messagesCount); } elseif ($bug->getProgress()->getProcessed() === $bug->getProgress()->getTotal()) { $this->recordAndReport($bug); } @@ -72,7 +52,7 @@ public function reduceSteps(int $bugId, int $length, int $from, int $to): void $reducer = $this->reducerManager->getReducer($this->config->getReducer()); $reducer->handle($bug, $from, $to); - $this->bugProgress->increaseProcessed($bug); + $this->bugRepository->increaseProcessed($bug); if ( $bug->getProgress()->getTotal() > 0 && $bug->getProgress()->getProcessed() === $bug->getProgress()->getTotal() @@ -84,7 +64,7 @@ public function reduceSteps(int $bugId, int $length, int $from, int $to): void public function reportBug(int $bugId): void { $bug = $this->getBug($bugId, 'report bug'); - $this->notifyHelper->notify($bug); + $this->bugNotifier->notify($bug); } /** @@ -93,33 +73,33 @@ public function reportBug(int $bugId): void public function recordVideo(int $bugId): void { $bug = $this->getBug($bugId, 'record video for bug'); - $driver = $this->selenoidHelper->createDriver($this->selenoidHelper->getCapabilities($bug, true)); + + if ($bug->getVideo()->isRecording()) { + throw new RecoverableMessageHandlingException( + sprintf('Can not record video for bug %d: bug is recording. Will retry later', $bug->getId()) + ); + } + try { - foreach ($bug->getSteps() as $step) { - $this->stepRunner->run($step, $bug->getTask()->getModelRevision(), $driver); - } - } catch (ExceptionInterface $exception) { - throw $exception; - } catch (Throwable $throwable) { - // Do nothing. + $this->bugRepository->startRecording($bug); + $bug->setDebug(true); + $this->stepsRunner->run( + $bug->getSteps(), + $bug, + function (Throwable $throwable) use ($bug) { + $bug->getVideo()->setErrorMessage( + $throwable->getMessage() !== $bug->getMessage() ? $throwable->getMessage() : null + ); + } + ); } finally { - $driver->quit(); + $this->bugRepository->stopRecording($bug); } } - public function createBug(array $steps, string $message): BugInterface - { - $bug = new Bug(); - $bug->setTitle(''); - $bug->setSteps($steps); - $bug->setMessage($message); - - return $bug; - } - protected function getBug(int $bugId, string $action): BugInterface { - $bug = $this->entityManager->find(Bug::class, $bugId); + $bug = $this->bugRepository->find($bugId); if (!$bug instanceof BugInterface) { throw new UnexpectedValueException(sprintf('Can not %s %d: bug not found', $action, $bugId)); diff --git a/src/Service/Bug/BugHelperInterface.php b/src/Service/Bug/BugHelperInterface.php index 274ce1ad..a979560d 100644 --- a/src/Service/Bug/BugHelperInterface.php +++ b/src/Service/Bug/BugHelperInterface.php @@ -2,8 +2,6 @@ namespace Tienvx\Bundle\MbtBundle\Service\Bug; -use Tienvx\Bundle\MbtBundle\Model\BugInterface; - interface BugHelperInterface { public function reduceBug(int $bugId): void; @@ -13,6 +11,4 @@ public function reduceSteps(int $bugId, int $length, int $from, int $to): void; public function reportBug(int $bugId): void; public function recordVideo(int $bugId): void; - - public function createBug(array $steps, string $message): BugInterface; } diff --git a/src/Service/Bug/BugProgress.php b/src/Service/Bug/BugProgress.php deleted file mode 100644 index 4414ecd0..00000000 --- a/src/Service/Bug/BugProgress.php +++ /dev/null @@ -1,43 +0,0 @@ -entityManager = $entityManager; - } - - public function increaseProcessed(BugInterface $bug, int $processed = 1): void - { - $callback = function () use ($bug, $processed): void { - // Refresh the bug for the latest progress. - $this->entityManager->refresh($bug); - - $this->entityManager->lock($bug, LockMode::PESSIMISTIC_WRITE); - $bug->getProgress()->increase($processed); - }; - - $this->entityManager->transactional($callback); - } - - public function increaseTotal(BugInterface $bug, int $total): void - { - $callback = function () use ($bug, $total): void { - // Refresh the bug for the latest progress. - $this->entityManager->refresh($bug); - - $this->entityManager->lock($bug, LockMode::PESSIMISTIC_WRITE); - $bug->getProgress()->setTotal($bug->getProgress()->getTotal() + $total); - }; - - $this->entityManager->transactional($callback); - } -} diff --git a/src/Service/Bug/BugProgressInterface.php b/src/Service/Bug/BugProgressInterface.php deleted file mode 100644 index 2f44a25b..00000000 --- a/src/Service/Bug/BugProgressInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -getTransitions() as $index => $transition) { - if ($transition instanceof TransitionInterface && 0 === count($transition->getFromPlaces())) { + if ($transition instanceof TransitionInterface && $transition->isStart()) { return $index; } } @@ -28,7 +28,7 @@ public function getStartTransitionId(RevisionInterface $revision): int public function getStartPlaceIds(RevisionInterface $revision): array { foreach ($revision->getTransitions() as $transition) { - if ($transition instanceof TransitionInterface && 0 === count($transition->getFromPlaces())) { + if ($transition instanceof TransitionInterface && $transition->isStart()) { return array_fill_keys($transition->getToPlaces(), 1); } } diff --git a/src/Service/Petrinet/MarkingHelper.php b/src/Service/Petrinet/MarkingHelper.php index 806f4d38..ba732867 100644 --- a/src/Service/Petrinet/MarkingHelper.php +++ b/src/Service/Petrinet/MarkingHelper.php @@ -4,20 +4,17 @@ use Petrinet\Builder\MarkingBuilder; use Petrinet\Model\MarkingInterface; -use Petrinet\Model\PetrinetInterface; use Petrinet\Model\PlaceMarkingInterface; use SingleColorPetrinet\Model\ColorfulFactoryInterface; use SingleColorPetrinet\Model\ColorfulMarkingInterface; use SingleColorPetrinet\Model\ColorInterface; +use SingleColorPetrinet\Model\PetrinetInterface; use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; class MarkingHelper implements MarkingHelperInterface { - protected ColorfulFactoryInterface $colorfulFactory; - - public function __construct(ColorfulFactoryInterface $colorfulFactory) + public function __construct(protected ColorfulFactoryInterface $colorfulFactory) { - $this->colorfulFactory = $colorfulFactory; } /** @@ -45,7 +42,7 @@ public function getMarking( ): ColorfulMarkingInterface { $markingBuilder = new MarkingBuilder($this->colorfulFactory); foreach ($places as $place => $tokens) { - $markingBuilder->mark($petrinet->getPlaces()[$place], $tokens); + $markingBuilder->mark($petrinet->getPlaceById($place), $tokens); } $marking = $markingBuilder->getMarking(); diff --git a/src/Service/Petrinet/MarkingHelperInterface.php b/src/Service/Petrinet/MarkingHelperInterface.php index 153b675a..bc6a7556 100644 --- a/src/Service/Petrinet/MarkingHelperInterface.php +++ b/src/Service/Petrinet/MarkingHelperInterface.php @@ -3,9 +3,9 @@ namespace Tienvx\Bundle\MbtBundle\Service\Petrinet; use Petrinet\Model\MarkingInterface; -use Petrinet\Model\PetrinetInterface; use SingleColorPetrinet\Model\ColorfulMarkingInterface; use SingleColorPetrinet\Model\ColorInterface; +use SingleColorPetrinet\Model\PetrinetInterface; interface MarkingHelperInterface { diff --git a/src/Service/Petrinet/PetrinetHelper.php b/src/Service/Petrinet/PetrinetHelper.php index a53ffe62..dc9ea438 100644 --- a/src/Service/Petrinet/PetrinetHelper.php +++ b/src/Service/Petrinet/PetrinetHelper.php @@ -2,12 +2,13 @@ namespace Tienvx\Bundle\MbtBundle\Service\Petrinet; -use Petrinet\Model\PetrinetInterface; use Petrinet\Model\PlaceInterface as PetrinetPlaceInterface; use Petrinet\Model\TransitionInterface as PetrinetTransitionInterface; use SingleColorPetrinet\Builder\SingleColorPetrinetBuilder; use SingleColorPetrinet\Model\ColorfulFactoryInterface; use SingleColorPetrinet\Model\ColorInterface; +use SingleColorPetrinet\Model\PetrinetInterface; +use Tienvx\AssignmentsEvaluator\AssignmentsEvaluator; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\PlaceInterface; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\TransitionInterface; use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; @@ -15,13 +16,13 @@ class PetrinetHelper implements PetrinetHelperInterface { - protected ColorfulFactoryInterface $colorfulFactory; - protected ExpressionLanguage $expressionLanguage; + public const FAKE_START_PLACE_ID = -1; - public function __construct(ColorfulFactoryInterface $colorfulFactory, ExpressionLanguage $expressionLanguage) - { - $this->colorfulFactory = $colorfulFactory; - $this->expressionLanguage = $expressionLanguage; + public function __construct( + protected ColorfulFactoryInterface $colorfulFactory, + protected ExpressionLanguage $expressionLanguage, + protected AssignmentsEvaluator $assignmentsEvaluator + ) { } public function build(RevisionInterface $revision): PetrinetInterface @@ -30,12 +31,16 @@ public function build(RevisionInterface $revision): PetrinetInterface $places = $this->getPlaces($revision, $builder); $transitions = $this->getTransitions($revision, $builder); foreach ($revision->getTransitions() as $index => $transition) { - if ($transition instanceof TransitionInterface && $this->isValidTransition($transition)) { - $this->connectPlacesToTransition( - array_intersect_key($places, array_flip($transition->getFromPlaces())), - $transitions[$index], - $builder - ); + if ($transition instanceof TransitionInterface) { + if ($transition->isStart()) { + $builder->connect($places[self::FAKE_START_PLACE_ID], $transitions[$index], 1); + } else { + $this->connectPlacesToTransition( + array_intersect_key($places, array_flip($transition->getFromPlaces())), + $transitions[$index], + $builder + ); + } $this->connectTransitionToPlaces($transitions[$index], $transition->getToPlaces(), $places, $builder); } } @@ -46,10 +51,10 @@ public function build(RevisionInterface $revision): PetrinetInterface protected function getPlaces(RevisionInterface $revision, SingleColorPetrinetBuilder $builder): array { $places = []; + $places[self::FAKE_START_PLACE_ID] = $builder->place(self::FAKE_START_PLACE_ID); foreach ($revision->getPlaces() as $index => $place) { if ($place instanceof PlaceInterface) { - $places[$index] = $builder->place(); - $places[$index]->setId($index); + $places[$index] = $builder->place($index); } } @@ -60,15 +65,22 @@ protected function getTransitions(RevisionInterface $revision, SingleColorPetrin { $transitions = []; foreach ($revision->getTransitions() as $index => $transition) { - if ($transition instanceof TransitionInterface && $this->isValidTransition($transition)) { - $guardCallback = $transition->getGuard() - ? fn (ColorInterface $color): bool => (bool) $this->expressionLanguage->evaluate( - $transition->getGuard(), - $color->getValues() - ) - : null; - $transitions[$index] = $builder->transition($guardCallback); - $transitions[$index]->setId($index); + if ($transition instanceof TransitionInterface) { + $transitions[$index] = $builder->transition( + $transition->getGuard() + ? fn (ColorInterface $color): bool => (bool) $this->expressionLanguage->evaluate( + $transition->getGuard(), + $color->getValues() + ) + : null, + $transition->getExpression() + ? fn (ColorInterface $color): array => $this->assignmentsEvaluator->evaluate( + $transition->getExpression(), + $color->getValues() + ) + : null, + $index + ); } } @@ -97,9 +109,4 @@ protected function connectTransitionToPlaces( $builder->connect($transition, $places[$toPlace], 1); } } - - protected function isValidTransition(TransitionInterface $transition): bool - { - return !empty($transition->getFromPlaces()); - } } diff --git a/src/Service/Petrinet/PetrinetHelperInterface.php b/src/Service/Petrinet/PetrinetHelperInterface.php index 8bcd4b35..e1c51196 100644 --- a/src/Service/Petrinet/PetrinetHelperInterface.php +++ b/src/Service/Petrinet/PetrinetHelperInterface.php @@ -2,7 +2,7 @@ namespace Tienvx\Bundle\MbtBundle\Service\Petrinet; -use Petrinet\Model\PetrinetInterface; +use SingleColorPetrinet\Model\PetrinetInterface; use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; interface PetrinetHelperInterface diff --git a/src/Service/SelenoidHelper.php b/src/Service/SelenoidHelper.php index 14747238..457b0ce1 100644 --- a/src/Service/SelenoidHelper.php +++ b/src/Service/SelenoidHelper.php @@ -5,13 +5,11 @@ use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\Remote\WebDriverCapabilityType; -use Tienvx\Bundle\MbtBundle\Model\BugInterface; -use Tienvx\Bundle\MbtBundle\Model\TaskInterface; +use Tienvx\Bundle\MbtBundle\Model\DebugInterface; class SelenoidHelper implements SelenoidHelperInterface { - private const BUG = 'bug'; - private const TASK = 'task'; + protected const WAIT_SECONDS = 2; protected string $webdriverUri; @@ -20,51 +18,56 @@ public function setWebdriverUri(string $webdriverUri): void $this->webdriverUri = $webdriverUri; } - public function getVideoUrl(TaskInterface|BugInterface $entity): string + public function getVideoUrl(DebugInterface $entity): string { - return sprintf('%s/video/%s', $this->webdriverUri, $this->getVideoName($entity)); + return sprintf('%s/video/%s', $this->webdriverUri, $entity->getVideoName()); } - public function getLogUrl(TaskInterface|BugInterface $entity): string + public function getLogUrl(DebugInterface $entity): string { - return sprintf('%s/logs/%s', $this->webdriverUri, $this->getLogName($entity)); + return sprintf('%s/logs/%s', $this->webdriverUri, $entity->getLogName()); } - public function createDriver(DesiredCapabilities $capabilities): RemoteWebDriver + public function createDriver(DebugInterface $entity): RemoteWebDriver { - return RemoteWebDriver::create( + $driver = $this->createDriverInternal( $this->webdriverUri . '/wd/hub', - $capabilities + $this->getCapabilities($entity) ); + if ($entity->isDebug()) { + $this->waitForVideoContainer(); + } + + return $driver; } - public function getCapabilities(TaskInterface|BugInterface $entity, bool $debug = false): DesiredCapabilities + protected function getCapabilities(DebugInterface $entity): DesiredCapabilities { - $task = $entity instanceof BugInterface ? $entity->getTask() : $entity; $caps = [ - WebDriverCapabilityType::BROWSER_NAME => $task->getBrowser()->getName(), - WebDriverCapabilityType::VERSION => $task->getBrowser()->getVersion(), + WebDriverCapabilityType::BROWSER_NAME => $entity->getTask()->getBrowser()->getName(), + WebDriverCapabilityType::VERSION => $entity->getTask()->getBrowser()->getVersion(), 'enableVNC' => false, - 'enableLog' => $debug, - 'enableVideo' => $debug, + 'enableLog' => $entity->isDebug(), + 'enableVideo' => $entity->isDebug(), ]; - if ($debug) { + if ($entity->isDebug()) { $caps += [ - 'logName' => $this->getLogName($entity), - 'videoName' => $this->getVideoName($entity), + 'logName' => $entity->getLogName(), + 'videoName' => $entity->getVideoName(), + 'videoFrameRate' => 60, ]; } return new DesiredCapabilities($caps); } - public function getVideoName(TaskInterface|BugInterface $entity): string + protected function waitForVideoContainer(): void { - return sprintf('%s-%d.mp4', $entity instanceof TaskInterface ? static::TASK : static::BUG, $entity->getId()); + sleep(static::WAIT_SECONDS); } - public function getLogName(TaskInterface|BugInterface $entity): string + protected function createDriverInternal($serverUrl, DesiredCapabilities $capabilities): RemoteWebDriver { - return sprintf('%s-%d.log', $entity instanceof TaskInterface ? static::TASK : static::BUG, $entity->getId()); + return RemoteWebDriver::create($serverUrl, $capabilities); } } diff --git a/src/Service/SelenoidHelperInterface.php b/src/Service/SelenoidHelperInterface.php index 58955dc2..93c7152d 100644 --- a/src/Service/SelenoidHelperInterface.php +++ b/src/Service/SelenoidHelperInterface.php @@ -2,24 +2,16 @@ namespace Tienvx\Bundle\MbtBundle\Service; -use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\Remote\RemoteWebDriver; -use Tienvx\Bundle\MbtBundle\Model\BugInterface; -use Tienvx\Bundle\MbtBundle\Model\TaskInterface; +use Tienvx\Bundle\MbtBundle\Model\DebugInterface; interface SelenoidHelperInterface { public function setWebdriverUri(string $webdriverUri): void; - public function getVideoUrl(TaskInterface|BugInterface $entity): string; + public function getVideoUrl(DebugInterface $entity): string; - public function getLogUrl(TaskInterface|BugInterface $entity): string; + public function getLogUrl(DebugInterface $entity): string; - public function createDriver(DesiredCapabilities $capabilities): RemoteWebDriver; - - public function getCapabilities(TaskInterface|BugInterface $entity, bool $debug = false): DesiredCapabilities; - - public function getVideoName(TaskInterface|BugInterface $entity): string; - - public function getLogName(TaskInterface|BugInterface $entity): string; + public function createDriver(DebugInterface $entity): RemoteWebDriver; } diff --git a/src/Service/ShortestPathStepsBuilder.php b/src/Service/ShortestPathStepsBuilder.php deleted file mode 100644 index 284beb58..00000000 --- a/src/Service/ShortestPathStepsBuilder.php +++ /dev/null @@ -1,62 +0,0 @@ -petrinetHelper = $petrinetHelper; - $this->petrinetDomainLogic = $petrinetDomainLogic; - } - - /** - * @throws ExceptionInterface - */ - public function create(BugInterface $bug, int $from, int $to): Generator - { - foreach ($bug->getSteps() as $index => $step) { - if ($index < $from) { - yield $step; - } - } - - yield from $this->getSteps($bug, $from, $to); - - foreach ($bug->getSteps() as $index => $step) { - if ($index > $to) { - yield $step; - } - } - } - - protected function getSteps(BugInterface $bug, int $from, int $to): iterable - { - $fromStep = $bug->getSteps()[$from] ?? null; - $toStep = $bug->getSteps()[$to] ?? null; - - if (!$fromStep instanceof StepInterface || !$toStep instanceof StepInterface) { - throw new OutOfRangeException('Can not create new steps using invalid range'); - } - - $this->petrinetDomainLogic->setPetrinet($this->petrinetHelper->build($bug->getTask()->getModelRevision())); - - yield from (new AStar($this->petrinetDomainLogic))->run($fromStep, $toStep); - - $this->petrinetDomainLogic->setPetrinet(null); - } -} diff --git a/src/Service/AStar/PetrinetDomainLogic.php b/src/Service/Step/Builder/PetrinetDomainLogic.php similarity index 54% rename from src/Service/AStar/PetrinetDomainLogic.php rename to src/Service/Step/Builder/PetrinetDomainLogic.php index 287c028c..6d3ec638 100644 --- a/src/Service/AStar/PetrinetDomainLogic.php +++ b/src/Service/Step/Builder/PetrinetDomainLogic.php @@ -1,46 +1,44 @@ transitionService = $transitionService; - $this->markingHelper = $markingHelper; - } - - public function setPetrinet(?PetrinetInterface $petrinet): void - { - $this->petrinet = $petrinet; } - public function calculateEstimatedCost(mixed $fromNode, mixed $toNode): float + public function calculateEstimatedCost(mixed $fromNode, mixed $toNode): float|int { if (!$fromNode instanceof Step || !$toNode instanceof Step) { throw new RuntimeException('The provided nodes are invalid'); } - $tokensDiff = []; - foreach ($toNode->getPlaces() as $place => $tokens) { - $tokensDiff[$place] = abs($tokens - ($fromNode->getPlaces()[$place] ?? 0)); + $tokensDiff = 0; + $fromPlaces = array_keys($fromNode->getPlaces()); + $toPlaces = array_keys($toNode->getPlaces()); + foreach (array_unique(array_merge($fromPlaces, $toPlaces)) as $place) { + if (!in_array($place, $fromPlaces)) { + $tokensDiff += $toNode->getPlaces()[$place]; + } elseif (!in_array($place, $toPlaces)) { + $tokensDiff += $fromNode->getPlaces()[$place]; + } else { + $tokensDiff += abs($toNode->getPlaces()[$place] - $fromNode->getPlaces()[$place]); + } } - // Estimate it will took N transitions to move N tokens if color is the same, twice if color is not the same. - return array_sum($tokensDiff) * (($fromNode->getColor()->getValues() != $toNode->getColor()->getValues()) + 1); + // Estimate it will took N transitions to move N tokens. + return $tokensDiff; } public function calculateRealCost(mixed $node, mixed $adjacent): float|int @@ -55,16 +53,12 @@ public function getAdjacentNodes(mixed $node): iterable throw new RuntimeException('The provided node is invalid'); } - if (!$this->petrinet instanceof PetrinetInterface) { - throw new RuntimeException('Petrinet is required'); - } - $adjacents = []; $marking = $this->markingHelper->getMarking($this->petrinet, $node->getPlaces(), $node->getColor()); foreach ($this->transitionService->getEnabledTransitions($this->petrinet, $marking) as $transition) { // Create new marking. $marking = $this->markingHelper->getMarking($this->petrinet, $node->getPlaces(), $node->getColor()); - if ($transition instanceof TransitionInterface && $marking instanceof ColorfulMarkingInterface) { + if ($transition instanceof TransitionInterface) { $this->transitionService->fire($transition, $marking); $adjacents[] = new Step( $this->markingHelper->getPlaces($marking), diff --git a/src/Service/Step/Builder/ShortestPathStepsBuilder.php b/src/Service/Step/Builder/ShortestPathStepsBuilder.php new file mode 100644 index 00000000..d3d4419d --- /dev/null +++ b/src/Service/Step/Builder/ShortestPathStepsBuilder.php @@ -0,0 +1,75 @@ +getSteps(), 0, $from); + $petrinet = $this->petrinetHelper->build($bug->getTask()->getModelRevision()); + $shortestSteps = $this->getShortestSteps($bug->getSteps(), $from, $to, $petrinet); + $lastStep = end($shortestSteps); + reset($shortestSteps); + yield from $shortestSteps; + yield from $this->getRemainingSteps(array_slice($bug->getSteps(), $to + 1), $lastStep, $petrinet); + } + + protected function getShortestSteps(array $steps, int $from, int $to, PetrinetInterface $petrinet): iterable + { + $fromStep = $steps[$from] ?? null; + $toStep = $steps[$to] ?? null; + + if (!$fromStep instanceof StepInterface || !$toStep instanceof StepInterface) { + throw new OutOfRangeException('Can not create shortest steps between invalid range'); + } + + return (new AStar(new PetrinetDomainLogic($this->transitionService, $this->markingHelper, $petrinet)))->run( + $fromStep, + $toStep + ); + } + + protected function getRemainingSteps(array $steps, StepInterface $lastStep, PetrinetInterface $petrinet): iterable + { + $marking = $this->markingHelper->getMarking($petrinet, $lastStep->getPlaces(), $lastStep->getColor()); + foreach ($steps as $step) { + if (!$step instanceof StepInterface) { + throw new OutOfRangeException('Remaining steps contains invalid step'); + } + $transition = $petrinet->getTransitionById($step->getTransition()); + if (!$this->transitionService->isEnabled($transition, $marking)) { + throw new StepsNotConnectedException('Can not connect remaining steps'); + } + $this->transitionService->fire($transition, $marking); + yield new Step( + $this->markingHelper->getPlaces($marking), + $marking->getColor(), + $step->getTransition() + ); + } + } +} diff --git a/src/Service/StepsBuilderInterface.php b/src/Service/Step/Builder/StepsBuilderInterface.php similarity index 76% rename from src/Service/StepsBuilderInterface.php rename to src/Service/Step/Builder/StepsBuilderInterface.php index 10b31fb9..c7a04810 100644 --- a/src/Service/StepsBuilderInterface.php +++ b/src/Service/Step/Builder/StepsBuilderInterface.php @@ -1,6 +1,6 @@ steps = []; + + return parent::start($entity); + } + + protected function catchException(callable $handleException, Throwable $throwable, ?StepInterface $step): void + { + if ($step instanceof StepInterface) { + // Last step cause the bug, we can't capture it. We capture it here. + $this->steps[] = clone $step; + } + $handleException($throwable, $this->steps); + } + + protected function stop(?RemoteWebDriver $driver): void + { + parent::stop($driver); + $this->steps = []; + } + + protected function runStep(StepInterface $step, RevisionInterface $revision, RemoteWebDriver $driver): bool + { + parent::runStep($step, $revision, $driver); + $this->steps[] = clone $step; + + return count($this->steps) < $this->config->getMaxSteps(); + } +} diff --git a/src/Service/StepRunner.php b/src/Service/Step/Runner/StepRunner.php similarity index 57% rename from src/Service/StepRunner.php rename to src/Service/Step/Runner/StepRunner.php index 616968ac..384ac1b2 100644 --- a/src/Service/StepRunner.php +++ b/src/Service/Step/Runner/StepRunner.php @@ -1,44 +1,48 @@ commandRunnerManager = $commandRunnerManager; } public function run(StepInterface $step, RevisionInterface $revision, RemoteWebDriver $driver): void { $transition = $revision->getTransition($step->getTransition()); if ($transition instanceof TransitionInterface) { - $this->executeCommands($transition->getCommands(), $step->getColor(), $driver); + $this->runCommands($transition->getCommands(), $driver); } foreach ($step->getPlaces() as $place => $tokens) { $place = $revision->getPlace($place); if ($place instanceof PlaceInterface) { - $this->executeCommands($place->getCommands(), $step->getColor(), $driver); + $this->runCommands($place->getCommands(), $driver); } } } - protected function executeCommands(array $commands, ColorInterface $color, RemoteWebDriver $driver): void + protected function runCommands(array $commands, RemoteWebDriver $driver): void { + $values = new Values(); foreach ($commands as $command) { if ($command instanceof CommandInterface) { - $this->commandRunnerManager->run($command, $color, $driver); + $this->commandManager->run( + $command->getCommand(), + $command->getTarget(), + $command->getValue(), + $values, + $driver + ); } } } diff --git a/src/Service/StepRunnerInterface.php b/src/Service/Step/Runner/StepRunnerInterface.php similarity index 84% rename from src/Service/StepRunnerInterface.php rename to src/Service/Step/Runner/StepRunnerInterface.php index baa3f1a7..8cc17a11 100644 --- a/src/Service/StepRunnerInterface.php +++ b/src/Service/Step/Runner/StepRunnerInterface.php @@ -1,6 +1,6 @@ start($entity); + foreach ($steps as $step) { + if (!$step instanceof StepInterface) { + throw new UnexpectedValueException(sprintf('Step must be instance of "%s".', StepInterface::class)); + } + if (!$this->runStep($step, $entity->getTask()->getModelRevision(), $driver)) { + break; + } + } + } catch (ExceptionInterface $exception) { + throw $exception; + } catch (Throwable $throwable) { + $this->catchException($handleException, $throwable, $step ?? null); + } finally { + $this->stop($driver ?? null); + } + } + + protected function start(DebugInterface $entity): RemoteWebDriver + { + return $this->selenoidHelper->createDriver($entity); + } + + protected function runStep(StepInterface $step, RevisionInterface $revision, RemoteWebDriver $driver): bool + { + $this->stepRunner->run($step, $revision, $driver); + + return true; + } + + abstract protected function catchException( + callable $handleException, + Throwable $throwable, + ?StepInterface $step + ): void; + + protected function stop(?RemoteWebDriver $driver): void + { + $driver?->quit(); + } +} diff --git a/src/Service/Step/Runner/StepsRunnerInterface.php b/src/Service/Step/Runner/StepsRunnerInterface.php new file mode 100644 index 00000000..18cc4ffb --- /dev/null +++ b/src/Service/Step/Runner/StepsRunnerInterface.php @@ -0,0 +1,10 @@ +generatorManager = $generatorManager; - $this->entityManager = $entityManager; - $this->stepRunner = $stepRunner; - $this->bugHelper = $bugHelper; - $this->selenoidHelper = $selenoidHelper; - $this->config = $config; } - /** - * @throws ExceptionInterface - */ public function run(int $taskId): void { - $task = $this->getTask($taskId); - $this->startRunning($task); - - $steps = []; - try { - $generator = $this->generatorManager->getGenerator($this->config->getGenerator()); - $driver = $this->selenoidHelper->createDriver( - $this->selenoidHelper->getCapabilities($task, $task->isDebug()) - ); - foreach ($generator->generate($task) as $step) { - if ($step instanceof StepInterface) { - $this->stepRunner->run($step, $task->getModelRevision(), $driver); - $steps[] = clone $step; - } - if (count($steps) >= $this->config->getMaxSteps()) { - break; - } - } - } catch (ExceptionInterface $exception) { - throw $exception; - } catch (Throwable $throwable) { - if (isset($step) && $step instanceof StepInterface) { - // Last step cause the bug, we can't capture it. We capture it here. - $steps[] = clone $step; - } - $task->addBug($this->bugHelper->createBug($steps, $throwable->getMessage())); - } finally { - if (isset($driver)) { - $driver->quit(); - } - $this->stopRunning($task); - } - } - - protected function getTask(int $taskId): TaskInterface - { - $task = $this->entityManager->find(Task::class, $taskId); + $task = $this->taskRepository->find($taskId); if (!$task instanceof TaskInterface) { - throw new UnexpectedValueException(sprintf('Can not execute task %d: task not found', $taskId)); + throw new UnexpectedValueException(sprintf('Can not run task %d: task not found', $taskId)); } - return $task; - } - - protected function startRunning(TaskInterface $task): void - { if ($task->isRunning()) { - throw new RuntimeException(sprintf('Task %d is already running', $task->getId())); - } else { - $task->setRunning(true); - $this->entityManager->flush(); + throw new RecoverableMessageHandlingException( + sprintf('Can not run task %d: task is running. Will retry later', $task->getId()) + ); } - } - protected function stopRunning(TaskInterface $task): void - { - $task->setRunning(false); - // Running task take long time. Reconnect to flush changes. - $this->entityManager->getConnection()->connect(); - $this->entityManager->flush(); + try { + $this->taskRepository->startRunning($task); + + $this->stepsRunner->run( + $this->generatorManager->getGenerator($this->config->getGenerator())->generate($task), + $task, + function (Throwable $throwable, array $steps) use ($taskId) { + $this->messageBus->dispatch(new CreateBugMessage( + $taskId, + $steps, + $throwable->getMessage() + )); + } + ); + } finally { + $this->taskRepository->stopRunning($task); + } } } diff --git a/src/TienvxMbtBundle.php b/src/TienvxMbtBundle.php index dc8f63e6..99c2b638 100644 --- a/src/TienvxMbtBundle.php +++ b/src/TienvxMbtBundle.php @@ -2,14 +2,44 @@ namespace Tienvx\Bundle\MbtBundle; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Tienvx\Bundle\MbtBundle\Command\CommandManager; use Tienvx\Bundle\MbtBundle\DependencyInjection\Compiler\PluginPass; +use Tienvx\Bundle\MbtBundle\Plugin\PluginInterface; +use Tienvx\Bundle\MbtBundle\Service\SelenoidHelper; -class TienvxMbtBundle extends Bundle +class TienvxMbtBundle extends AbstractBundle { + public const WEBDRIVER_URI = 'webdriver_uri'; + public const UPLOAD_DIR = 'upload_dir'; + public function build(ContainerBuilder $container): void { $container->addCompilerPass(new PluginPass()); } + + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../config/definition.php'); + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + $container->import('../config/services.php'); + + $container->services() + ->get(SelenoidHelper::class) + ->call('setWebdriverUri', [$config[static::WEBDRIVER_URI]]) + ; + $container->services() + ->get(CommandManager::class) + ->call('setUploadDir', [$config[static::UPLOAD_DIR]]) + ; + $builder->registerForAutoconfiguration(PluginInterface::class) + ->setLazy(true) + ->addTag(PluginInterface::TAG); + } } diff --git a/src/Validator/Tags.php b/src/Validator/Tags.php index 973268b0..0995f09f 100644 --- a/src/Validator/Tags.php +++ b/src/Validator/Tags.php @@ -4,9 +4,7 @@ use Symfony\Component\Validator\Constraint; -/** - * @Annotation - */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class Tags extends Constraint { public const IS_TAGS_INVALID_ERROR = '628fca96-35f8-11eb-adc1-0242ac120002'; diff --git a/src/Validator/ValidCommand.php b/src/Validator/ValidCommand.php index 438014f7..fd519080 100644 --- a/src/Validator/ValidCommand.php +++ b/src/Validator/ValidCommand.php @@ -4,9 +4,7 @@ use Symfony\Component\Validator\Constraint; -/** - * @Annotation - */ +#[\Attribute(\Attribute::TARGET_CLASS)] class ValidCommand extends Constraint { public const IS_COMMAND_INVALID_ERROR = 'ba5fd751-cbdf-45ab-a1e7-37045d5ef44b'; @@ -15,12 +13,13 @@ class ValidCommand extends Constraint self::IS_COMMAND_INVALID_ERROR => 'IS_COMMAND_INVALID_ERROR', ]; - public string $commandMessage = 'mbt.model.command.invalid_command'; + public string $invalidCommandMessage = 'mbt.model.command.invalid_command'; public string $targetRequiredMessage = 'mbt.model.command.required_target'; public string $targetInvalidMessage = 'mbt.model.command.invalid_target'; public string $valueRequiredMessage = 'mbt.model.command.required_value'; + public string $valueInvalidMessage = 'mbt.model.command.invalid_value'; - public function getTargets() + public function getTargets(): string|array { return self::CLASS_CONSTRAINT; } diff --git a/src/Validator/ValidCommandValidator.php b/src/Validator/ValidCommandValidator.php index 6f178871..734c7e86 100644 --- a/src/Validator/ValidCommandValidator.php +++ b/src/Validator/ValidCommandValidator.php @@ -6,16 +6,13 @@ use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; -use Tienvx\Bundle\MbtBundle\Command\CommandRunnerManagerInterface; +use Tienvx\Bundle\MbtBundle\Command\CommandManagerInterface; use Tienvx\Bundle\MbtBundle\ValueObject\Model\Command; class ValidCommandValidator extends ConstraintValidator { - protected CommandRunnerManagerInterface $commandRunnerManager; - - public function __construct(CommandRunnerManagerInterface $commandRunnerManager) + public function __construct(protected CommandManagerInterface $commandManager) { - $this->commandRunnerManager = $commandRunnerManager; } public function validate($value, Constraint $constraint) @@ -32,35 +29,35 @@ public function validate($value, Constraint $constraint) throw new UnexpectedValueException($value, Command::class); } - if (!in_array($value->getCommand(), $this->commandRunnerManager->getAllCommands())) { - $this->context->buildViolation($constraint->commandMessage) + if (!$this->commandManager->hasCommand($value->getCommand())) { + $this->context->buildViolation($constraint->invalidCommandMessage) ->setCode(ValidCommand::IS_COMMAND_INVALID_ERROR) ->atPath('command') ->addViolation(); } - if (in_array($value->getCommand(), $this->commandRunnerManager->getCommandsRequireTarget())) { - if (is_null($value->getTarget())) { - $this->context->buildViolation($constraint->targetRequiredMessage) - ->setCode(ValidCommand::IS_COMMAND_INVALID_ERROR) - ->atPath('target') - ->addViolation(); - } elseif (!$this->commandRunnerManager->validateTarget($value)) { - $this->context->buildViolation($constraint->targetInvalidMessage) - ->setCode(ValidCommand::IS_COMMAND_INVALID_ERROR) - ->atPath('target') - ->addViolation(); - } + if ($this->commandManager->isTargetMissing($value->getCommand(), $value->getTarget())) { + $this->context->buildViolation($constraint->targetRequiredMessage) + ->setCode(ValidCommand::IS_COMMAND_INVALID_ERROR) + ->atPath('target') + ->addViolation(); + } elseif ($this->commandManager->isTargetNotValid($value->getCommand(), $value->getTarget())) { + $this->context->buildViolation($constraint->targetInvalidMessage) + ->setCode(ValidCommand::IS_COMMAND_INVALID_ERROR) + ->atPath('target') + ->addViolation(); } - if ( - in_array($value->getCommand(), $this->commandRunnerManager->getCommandsRequireValue()) - && is_null($value->getValue()) - ) { + if ($this->commandManager->isValueMissing($value->getCommand(), $value->getValue())) { $this->context->buildViolation($constraint->valueRequiredMessage) ->setCode(ValidCommand::IS_COMMAND_INVALID_ERROR) ->atPath('value') ->addViolation(); + } elseif ($this->commandManager->isValueNotValid($value->getCommand(), $value->getValue())) { + $this->context->buildViolation($constraint->valueInvalidMessage) + ->setCode(ValidCommand::IS_COMMAND_INVALID_ERROR) + ->atPath('value') + ->addViolation(); } } } diff --git a/src/ValueObject/Bug/Step.php b/src/ValueObject/Bug/Step.php index cb89bca7..7da148b0 100644 --- a/src/ValueObject/Bug/Step.php +++ b/src/ValueObject/Bug/Step.php @@ -2,27 +2,22 @@ namespace Tienvx\Bundle\MbtBundle\ValueObject\Bug; +use SingleColorPetrinet\Model\Color; use SingleColorPetrinet\Model\ColorInterface; use Symfony\Component\Validator\Constraints as Assert; use Tienvx\Bundle\MbtBundle\Model\Bug\Step as StepModel; class Step extends StepModel { - /** - * @Assert\All({ - * @Assert\Type("integer") - * }) - * @Assert\Count(min=1, minMessage="mbt.bug.missing_places_in_step") - */ + #[Assert\All([ + new Assert\Type('integer'), + ])] + #[Assert\Count(min: 1, minMessage: 'mbt.bug.missing_places_in_step')] protected array $places; - /** - * @Assert\Type("\SingleColorPetrinet\Model\Color") - */ + #[Assert\Type(type: Color::class)] protected ColorInterface $color; - /** - * @Assert\Type("integer") - */ + #[Assert\Type('integer')] protected int $transition; } diff --git a/src/ValueObject/Model/Command.php b/src/ValueObject/Model/Command.php index 3b732872..315bb202 100644 --- a/src/ValueObject/Model/Command.php +++ b/src/ValueObject/Model/Command.php @@ -6,14 +6,10 @@ use Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command as CommandModel; use Tienvx\Bundle\MbtBundle\Validator\ValidCommand; -/** - * @ValidCommand - */ +#[ValidCommand] class Command extends CommandModel { - /** - * @Assert\NotBlank - */ + #[Assert\NotBlank] protected string $command; protected ?string $target = null; diff --git a/src/ValueObject/Model/Place.php b/src/ValueObject/Model/Place.php index dddcafa2..f9062f57 100644 --- a/src/ValueObject/Model/Place.php +++ b/src/ValueObject/Model/Place.php @@ -7,16 +7,12 @@ class Place extends PlaceModel { - /** - * @Assert\NotBlank - */ + #[Assert\NotBlank] protected string $label = ''; - /** - * @Assert\All({ - * @Assert\Type("\Tienvx\Bundle\MbtBundle\ValueObject\Model\Command") - * }) - * @Assert\Valid - */ + #[Assert\All([ + new Assert\Type(type: Command::class), + ])] + #[Assert\Valid] protected array $commands = []; } diff --git a/src/ValueObject/Model/Transition.php b/src/ValueObject/Model/Transition.php index 0eb0023e..694e376c 100644 --- a/src/ValueObject/Model/Transition.php +++ b/src/ValueObject/Model/Transition.php @@ -3,43 +3,40 @@ namespace Tienvx\Bundle\MbtBundle\ValueObject\Model; use Symfony\Component\Validator\Constraints as Assert; +use Tienvx\Bundle\AssignmentsEvaluatorBundle\Validator\AssignmentsSyntax; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\Transition as TransitionModel; class Transition extends TransitionModel { - /** - * @Assert\NotBlank - */ + #[Assert\NotBlank] protected string $label = ''; - /** - * @Assert\AtLeastOneOf({ - * @Assert\IsNull, - * @Assert\ExpressionLanguageSyntax - * }) - */ + #[Assert\AtLeastOneOf([ + new Assert\IsNull(), + new Assert\ExpressionSyntax(), + ])] protected ?string $guard = null; - /** - * @Assert\All({ - * @Assert\Type("\Tienvx\Bundle\MbtBundle\ValueObject\Model\Command") - * }) - * @Assert\Valid - */ + #[Assert\AtLeastOneOf([ + new Assert\IsNull(), + new AssignmentsSyntax(), + ])] + protected ?string $expression = null; + + #[Assert\All([ + new Assert\Type(Command::class), + ])] + #[Assert\Valid] protected array $commands = []; - /** - * @Assert\All({ - * @Assert\Type("integer") - * }) - */ + #[Assert\All([ + new Assert\Type('integer'), + ])] protected array $fromPlaces = []; - /** - * @Assert\All({ - * @Assert\Type("integer") - * }) - * @Assert\Count(min=1, minMessage="mbt.model.missing_to_places") - */ + #[Assert\All([ + new Assert\Type('integer'), + ])] + #[Assert\Count(min: 1, minMessage: 'mbt.model.missing_to_places')] protected array $toPlaces = []; } diff --git a/tests/Channel/AbstractChannelTest.php b/tests/Channel/AbstractChannelTest.php new file mode 100644 index 00000000..3ca0cf87 --- /dev/null +++ b/tests/Channel/AbstractChannelTest.php @@ -0,0 +1,18 @@ +assertSame(ChannelManager::class, AbstractChannel::getManager()); + } +} diff --git a/tests/Channel/ChannelManagerTest.php b/tests/Channel/ChannelManagerTest.php index c527ff48..08e35235 100644 --- a/tests/Channel/ChannelManagerTest.php +++ b/tests/Channel/ChannelManagerTest.php @@ -2,33 +2,34 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Channel; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ServiceLocator; +use Tienvx\Bundle\MbtBundle\Channel\ChannelInterface; use Tienvx\Bundle\MbtBundle\Channel\ChannelManager; -use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; +use Tienvx\Bundle\MbtBundle\Plugin\PluginInterface; +use Tienvx\Bundle\MbtBundle\Plugin\PluginManagerInterface; +use Tienvx\Bundle\MbtBundle\Tests\Plugin\PluginManagerTest; /** * @covers \Tienvx\Bundle\MbtBundle\Channel\ChannelManager - * @covers \Tienvx\Bundle\MbtBundle\Plugin\AbstractPluginManager + * + * @uses \Tienvx\Bundle\MbtBundle\Plugin\PluginManager */ -class ChannelManagerTest extends TestCase +class ChannelManagerTest extends PluginManagerTest { - protected ChannelManager $channelManager; - protected ServiceLocator $locator; + protected array $plugins = ['email', 'slack/chat']; + protected string $getMethod = 'getChannel'; - protected function setUp(): void + protected function createPluginManager(): PluginManagerInterface { - $this->locator = $this->createMock(ServiceLocator::class); - $plugins = ['split', 'random']; - $this->channelManager = new ChannelManager($this->locator, $plugins); + return new ChannelManager($this->locator, $this->plugins); } - public function testDoesNotHaveOther(): void + protected function createPlugin(): PluginInterface { - $this->locator->expects($this->never())->method('has'); - $this->assertFalse($this->channelManager->has('other')); - $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('Channel "other" does not exist.'); - $this->channelManager->getChannel('other'); + return $this->createMock(ChannelInterface::class); + } + + protected function getInvalidPluginExceptionMessage(string $plugin): string + { + return sprintf('Channel "%s" does not exist.', $plugin); } } diff --git a/tests/Command/Alert/AcceptAlertCommandTest.php b/tests/Command/Alert/AcceptAlertCommandTest.php new file mode 100644 index 00000000..94d12a56 --- /dev/null +++ b/tests/Command/Alert/AcceptAlertCommandTest.php @@ -0,0 +1,55 @@ +createMock(WebDriverAlert::class); + $alert->expects($this->once())->method('accept'); + $targetLocator = $this->createMock(RemoteTargetLocator::class); + $targetLocator->expects($this->once())->method('alert')->willReturn($alert); + $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); + $this->command->run(null, null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Alert/AnswerPromptCommandTest.php b/tests/Command/Alert/AnswerPromptCommandTest.php new file mode 100644 index 00000000..88575404 --- /dev/null +++ b/tests/Command/Alert/AnswerPromptCommandTest.php @@ -0,0 +1,56 @@ +createMock(WebDriverAlert::class); + $alert->expects($this->once())->method('sendKeys')->with('Yes, I agree'); + $alert->expects($this->once())->method('accept'); + $targetLocator = $this->createMock(RemoteTargetLocator::class); + $targetLocator->expects($this->once())->method('alert')->willReturn($alert); + $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); + $this->command->run('Yes, I agree', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Alert/DismissPromptCommandTest.php b/tests/Command/Alert/DismissPromptCommandTest.php new file mode 100644 index 00000000..3d3c1eed --- /dev/null +++ b/tests/Command/Alert/DismissPromptCommandTest.php @@ -0,0 +1,55 @@ +createMock(WebDriverAlert::class); + $alert->expects($this->once())->method('dismiss'); + $targetLocator = $this->createMock(RemoteTargetLocator::class); + $targetLocator->expects($this->once())->method('alert')->willReturn($alert); + $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); + $this->command->run(null, null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertAlertCommandTest.php b/tests/Command/Assert/AssertAlertCommandTest.php new file mode 100644 index 00000000..bd0e3193 --- /dev/null +++ b/tests/Command/Assert/AssertAlertCommandTest.php @@ -0,0 +1,70 @@ +expectExceptionObject($exception); + } + $alert = $this->createMock(WebDriverAlert::class); + $alert->expects($this->once())->method('getText')->willReturn('expected alert'); + $locator = $this->createMock(RemoteTargetLocator::class); + $locator->expects($this->once())->method('alert')->willReturn($alert); + $this->driver->expects($this->once())->method('switchTo')->willReturn($locator); + $this->command->run($target, null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['expected alert', null], + ['unexpected alert', new Exception('Actual alert text "expected alert" did not match "unexpected alert"')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertCheckedCommandTest.php b/tests/Command/Assert/AssertCheckedCommandTest.php new file mode 100644 index 00000000..34a7c480 --- /dev/null +++ b/tests/Command/Assert/AssertCheckedCommandTest.php @@ -0,0 +1,75 @@ +expectExceptionObject($exception); + } + $this->element->expects($this->once())->method('isSelected')->willReturn($isSelected); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.term-and-condition' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('css=.term-and-condition', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [true, null], + [false, new Exception('Element "css=.term-and-condition" is not checked, expected to be checked')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertCommandTest.php b/tests/Command/Assert/AssertCommandTest.php new file mode 100644 index 00000000..57d3ba8b --- /dev/null +++ b/tests/Command/Assert/AssertCommandTest.php @@ -0,0 +1,69 @@ + 'value1', 'key2' => 'value2']); + if ($exception) { + $this->expectExceptionObject($exception); + } else { + $this->expectNotToPerformAssertions(); + } + $this->command->run($target, $value, $values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['key1', 'value1', null], + ['key2', 'value3', new Exception('Actual value "value2" did not match "value3"')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertEditableCommandTest.php b/tests/Command/Assert/AssertEditableCommandTest.php new file mode 100644 index 00000000..badfd9b4 --- /dev/null +++ b/tests/Command/Assert/AssertEditableCommandTest.php @@ -0,0 +1,86 @@ +expectExceptionObject($exception); + } + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'name' === $selector->getMechanism() + && 'username' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->driver + ->expects($this->once()) + ->method('executeScript') + ->with( + 'return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', + [$this->element] + ) + ->willReturn((object) ['enabled' => $enabled, 'readonly' => $readonly]); + $this->command->run('name=username', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + $exception = new Exception('Element "name=username" is not editable'); + + return [ + [true, false, null], + [true, true, $exception], + [false, true, $exception], + [false, false, $exception], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertElementNotPresentCommandTest.php b/tests/Command/Assert/AssertElementNotPresentCommandTest.php new file mode 100644 index 00000000..6eab0095 --- /dev/null +++ b/tests/Command/Assert/AssertElementNotPresentCommandTest.php @@ -0,0 +1,79 @@ +expectExceptionObject($exception); + } + $this->driver + ->expects($this->once()) + ->method('findElements') + ->with( + $this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.cart' === $selector->getValue(); + }) + ) + ->willReturn(array_fill(0, $count, $this->element)); + $this->command->run('css=.cart', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + $exception = new Exception('Unexpected element "css=.cart" was found in page'); + + return [ + [0, null], + [1, $exception], + [2, $exception], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertElementPresentCommandTest.php b/tests/Command/Assert/AssertElementPresentCommandTest.php new file mode 100644 index 00000000..81fbb7e3 --- /dev/null +++ b/tests/Command/Assert/AssertElementPresentCommandTest.php @@ -0,0 +1,79 @@ +expectExceptionObject($exception); + } + $this->driver + ->expects($this->once()) + ->method('findElements') + ->with( + $this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.cart' === $selector->getValue(); + }) + ) + ->willReturn(array_fill(0, $count, $this->element)); + $this->command->run('css=.cart', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + $exception = new Exception('Expected element "css=.cart" was not found in page'); + + return [ + [0, $exception], + [1, null], + [2, null], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertNotCheckedCommandTest.php b/tests/Command/Assert/AssertNotCheckedCommandTest.php new file mode 100644 index 00000000..60050431 --- /dev/null +++ b/tests/Command/Assert/AssertNotCheckedCommandTest.php @@ -0,0 +1,75 @@ +expectExceptionObject($exception); + } + $this->element->expects($this->once())->method('isSelected')->willReturn($isSelected); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.term-and-condition' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('css=.term-and-condition', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [false, null], + [true, new Exception('Element "css=.term-and-condition" is checked, expected to be unchecked')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertNotEditableCommandTest.php b/tests/Command/Assert/AssertNotEditableCommandTest.php new file mode 100644 index 00000000..3e0b5572 --- /dev/null +++ b/tests/Command/Assert/AssertNotEditableCommandTest.php @@ -0,0 +1,86 @@ +expectExceptionObject($exception); + } + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'name' === $selector->getMechanism() + && 'username' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->driver + ->expects($this->once()) + ->method('executeScript') + ->with( + 'return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', + [$this->element] + ) + ->willReturn((object) ['enabled' => $enabled, 'readonly' => $readonly]); + $this->command->run('name=username', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + $exception = new Exception('Element "name=username" is editable'); + + return [ + [true, false, $exception], + [true, true, null], + [false, true, null], + [false, false, null], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertNotSelectedLabelCommandTest.php b/tests/Command/Assert/AssertNotSelectedLabelCommandTest.php new file mode 100644 index 00000000..07c1fbbb --- /dev/null +++ b/tests/Command/Assert/AssertNotSelectedLabelCommandTest.php @@ -0,0 +1,82 @@ +expectExceptionObject($exception); + } + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'partial link text' === $selector->getMechanism() + && 'Language' === $selector->getValue(); + })) + ->willReturn($this->element); + $option = $this->createMock(WebDriverElement::class); + $option->expects($this->once())->method('getText')->willReturn($actual); + $select = $this->createMock(WebDriverSelect::class); + $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); + $command = $this->createPartialMock(AssertNotSelectedLabelCommand::class, ['getSelect']); + $command->expects($this->once())->method('getSelect')->with($this->element)->willReturn($select); + $command->run('partialLinkText=Language', 'United Kingdom', $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['United States', null], + ['United Kingdom', new Exception('Actual label "United Kingdom" did match "United Kingdom"')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertNotSelectedValueCommandTest.php b/tests/Command/Assert/AssertNotSelectedValueCommandTest.php new file mode 100644 index 00000000..8e56755e --- /dev/null +++ b/tests/Command/Assert/AssertNotSelectedValueCommandTest.php @@ -0,0 +1,82 @@ +expectExceptionObject($exception); + } + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'partial link text' === $selector->getMechanism() + && 'Language' === $selector->getValue(); + })) + ->willReturn($this->element); + $option = $this->createMock(WebDriverElement::class); + $option->expects($this->once())->method('getAttribute')->with('value')->willReturn($actual); + $select = $this->createMock(WebDriverSelect::class); + $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); + $command = $this->createPartialMock(AssertNotSelectedValueCommand::class, ['getSelect']); + $command->expects($this->once())->method('getSelect')->with($this->element)->willReturn($select); + $command->run('partialLinkText=Language', 'en_GB', $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['en_US', null], + ['en_GB', new Exception('Actual value "en_GB" did match "en_GB"')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertNotTextCommandTest.php b/tests/Command/Assert/AssertNotTextCommandTest.php new file mode 100644 index 00000000..06d530da --- /dev/null +++ b/tests/Command/Assert/AssertNotTextCommandTest.php @@ -0,0 +1,78 @@ +expectExceptionObject($exception); + } + $this->element->expects($this->once())->method('getText')->willReturn($actual); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'xpath' === $selector->getMechanism() + && '//h4[@href="#"]' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('xpath=//h4[@href="#"]', 'Welcome to our store', $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['Goodbye! See you again', null], + [ + 'Welcome to our store', + new Exception('Actual text "Welcome to our store" did match "Welcome to our store"'), + ], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertSelectedLabelCommandTest.php b/tests/Command/Assert/AssertSelectedLabelCommandTest.php new file mode 100644 index 00000000..dd046ca8 --- /dev/null +++ b/tests/Command/Assert/AssertSelectedLabelCommandTest.php @@ -0,0 +1,82 @@ +expectExceptionObject($exception); + } + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'partial link text' === $selector->getMechanism() + && 'Language' === $selector->getValue(); + })) + ->willReturn($this->element); + $option = $this->createMock(WebDriverElement::class); + $option->expects($this->once())->method('getText')->willReturn($actual); + $select = $this->createMock(WebDriverSelect::class); + $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); + $command = $this->createPartialMock(AssertSelectedLabelCommand::class, ['getSelect']); + $command->expects($this->once())->method('getSelect')->with($this->element)->willReturn($select); + $command->run('partialLinkText=Language', 'United Kingdom', $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['United Kingdom', null], + ['United States', new Exception('Actual label "United States" did not match "United Kingdom"')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertSelectedValueCommandTest.php b/tests/Command/Assert/AssertSelectedValueCommandTest.php new file mode 100644 index 00000000..7a697f46 --- /dev/null +++ b/tests/Command/Assert/AssertSelectedValueCommandTest.php @@ -0,0 +1,82 @@ +expectExceptionObject($exception); + } + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'partial link text' === $selector->getMechanism() + && 'Language' === $selector->getValue(); + })) + ->willReturn($this->element); + $option = $this->createMock(WebDriverElement::class); + $option->expects($this->once())->method('getAttribute')->with('value')->willReturn($actual); + $select = $this->createMock(WebDriverSelect::class); + $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); + $command = $this->createPartialMock(AssertSelectedValueCommand::class, ['getSelect']); + $command->expects($this->once())->method('getSelect')->with($this->element)->willReturn($select); + $command->run('partialLinkText=Language', 'en_GB', $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['en_GB', null], + ['en_US', new Exception('Actual value "en_US" did not match "en_GB"')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertTextCommandTest.php b/tests/Command/Assert/AssertTextCommandTest.php new file mode 100644 index 00000000..a07c32a0 --- /dev/null +++ b/tests/Command/Assert/AssertTextCommandTest.php @@ -0,0 +1,78 @@ +expectExceptionObject($exception); + } + $this->element->expects($this->once())->method('getText')->willReturn($actual); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'xpath' === $selector->getMechanism() + && '//h4[@href="#"]' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('xpath=//h4[@href="#"]', 'Welcome to our store', $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['Welcome to our store', null], + [ + 'Goodbye! See you again', + new Exception('Actual text "Goodbye! See you again" did not match "Welcome to our store"'), + ], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertTitleCommandTest.php b/tests/Command/Assert/AssertTitleCommandTest.php new file mode 100644 index 00000000..9cba5485 --- /dev/null +++ b/tests/Command/Assert/AssertTitleCommandTest.php @@ -0,0 +1,64 @@ +expectExceptionObject($exception); + } + $this->driver->expects($this->exactly(2))->method('getTitle')->willReturn($actual); + $this->command->run('Welcome', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['Welcome', null], + ['Goodbye', new Exception('Actual title "Goodbye" did not match "Welcome"')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Assert/AssertValueCommandTest.php b/tests/Command/Assert/AssertValueCommandTest.php new file mode 100644 index 00000000..d2290004 --- /dev/null +++ b/tests/Command/Assert/AssertValueCommandTest.php @@ -0,0 +1,79 @@ +expectExceptionObject($exception); + } + $this->element + ->expects($this->once()) + ->method('getAttribute') + ->with('value') + ->willReturn($actual); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.quality' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('css=.quality', '14', $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['14', null], + ['15', new Exception('Actual value "15" did not match "14"')], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/CommandManagerTest.php b/tests/Command/CommandManagerTest.php new file mode 100644 index 00000000..8b286486 --- /dev/null +++ b/tests/Command/CommandManagerTest.php @@ -0,0 +1,211 @@ +httpClient = $this->createMock(HttpClientInterface::class); + $this->manager = new CommandManager($this->httpClient); + $this->driver = $this->createMock(RemoteWebDriver::class); + $this->values = $this->createMock(ValuesInterface::class); + } + + public function testHasCommand(): void + { + $this->assertFalse($this->manager->hasCommand('test')); + $this->assertTrue($this->manager->hasCommand('open')); + } + + public function testIsTargetMissing(): void + { + $this->assertFalse($this->manager->isTargetMissing('sendKeys', 'name=first-name')); + $this->assertTrue($this->manager->isTargetMissing('sendKeys', '')); + } + + public function testIsTargetNotValid(): void + { + $this->assertTrue($this->manager->isTargetNotValid('sendKeys', '.gender')); + $this->assertFalse($this->manager->isTargetNotValid('sendKeys', 'id=email')); + } + + public function testIsValueMissing(): void + { + $this->assertFalse($this->manager->isValueMissing('sendKeys', 'First Name')); + $this->assertTrue($this->manager->isValueMissing('sendKeys', '')); + } + + public function testIsValueNotValid(): void + { + $this->assertTrue($this->manager->isValueNotValid('clickAt', '123x456')); + $this->assertFalse($this->manager->isValueNotValid('clickAt', '67,89')); + } + + public function testRunCommand(): void + { + $command = 'command'; + $target = 'target'; + $value = 'value'; + $commandObject = $this->createMock(CommandInterface::class); + $commandObject + ->expects($this->once()) + ->method('run') + ->with('processed ' . $target, 'processed ' . $value, $this->values, $this->driver); + $this->manager = $this->createPartialMock(CommandManager::class, ['createCommand', 'process']); + $this->manager + ->expects($this->once()) + ->method('createCommand') + ->with($command) + ->willReturn($commandObject); + $this->manager + ->expects($this->exactly(2)) + ->method('process') + ->withConsecutive( + [$target, $this->values], + [$value, $this->values] + ) + ->willReturnCallback(fn (string $text) => 'processed ' . $text); + $this->manager->run($command, $target, $value, $this->values, $this->driver); + } + + /** + * @dataProvider textProvider + */ + public function testProcess(?string $text, array $values, ?string $expected): void + { + $method = (new \ReflectionMethod($this->manager, 'process')); + $this->assertSame($expected, $method->invoke($this->manager, $text, new Values($values))); + } + + public function textProvider(): array + { + return [ + [ + null, + [ + 'key1' => 'value1', + 'key2' => 'value2', + ], + null, + ], + [ + '', + [ + 'key1' => 'value1', + 'key2' => 'value2', + ], + '', + ], + [ + 'css=.id', + [ + 'key1' => 'value1', + 'key2' => 'value2', + ], + 'css=.id', + ], + [ + '${key}', + [ + 'key' => 'status', + ], + 'status', + ], + [ + 'variable1', + [ + 'variable1' => 123, + ], + 'variable1', + ], + [ + 'xpath=//div[text()=\'${variable1}\']/input', + [ + 'variable1' => 'value1', + 'variable2' => 'value2', + ], + 'xpath=//div[text()=\'value1\']/input', + ], + [ + 'http://example.com/${path}', + [], + 'http://example.com/path', + ], + ]; + } + + public function testCreateUploadCommand(): void + { + $this->manager->setUploadDir('/path/to/upload/files'); + $method = (new \ReflectionMethod($this->manager, 'createCommand')); + $command = $method->invoke($this->manager, 'upload'); + $property = (new \ReflectionProperty($command, 'uploadDir')); + $this->assertInstanceOf(UploadCommand::class, $command); + $this->assertSame('/path/to/upload/files', $property->getValue($command)); + } + + /** + * @dataProvider commandProvider + */ + public function testCreateHasHttpClientCommand(string $command, string $class): void + { + $method = (new \ReflectionMethod($this->manager, 'createCommand')); + $command = $method->invoke($this->manager, $command); + $property = (new \ReflectionProperty($command, 'httpClient')); + $this->assertInstanceOf($class, $command); + $this->assertSame($this->httpClient, $property->getValue($command)); + } + + public function commandProvider(): array + { + return [ + ['assertFileDownloaded', AssertFileDownloadedCommand::class], + ['assertClipboard', AssertClipboardCommand::class], + ['updateClipboard', UpdateClipboardCommand::class], + ]; + } + + public function testCreateValidCommand(): void + { + $method = (new \ReflectionMethod($this->manager, 'createCommand')); + $this->assertInstanceOf(ClickCommand::class, $method->invoke($this->manager, 'click')); + } + + public function testCreateInvalidCommand(): void + { + $method = (new \ReflectionMethod($this->manager, 'createCommand')); + $this->expectExceptionObject(new OutOfRangeException('Command newCommand not found')); + $method->invoke($this->manager, 'newCommand'); + } +} diff --git a/tests/Command/CommandPreprocessorTest.php b/tests/Command/CommandPreprocessorTest.php deleted file mode 100644 index ea43b306..00000000 --- a/tests/Command/CommandPreprocessorTest.php +++ /dev/null @@ -1,54 +0,0 @@ -setCommand(AssertionRunner::ASSERT); - $command->setTarget('${variable}'); - $command->setValue('value'); - $color = new Color(); - $color->setValues([ - 'variable' => 'key', - 'key' => 'value', - ]); - $preprocessor = new CommandPreprocessor(); - $newCommand = $preprocessor->process($command, $color); - $this->assertSame(AssertionRunner::ASSERT, $newCommand->getCommand()); - $this->assertSame('key', $newCommand->getTarget()); - $this->assertSame('value', $newCommand->getValue()); - } - - public function testProcessEmpty(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::CLICK); - $command->setTarget(''); - $command->setValue(null); - $color = new Color(); - $color->setValues([ - 'variable' => 'key', - 'key' => 'value', - ]); - $preprocessor = new CommandPreprocessor(); - $newCommand = $preprocessor->process($command, $color); - $this->assertSame(MouseCommandRunner::CLICK, $newCommand->getCommand()); - $this->assertSame('', $newCommand->getTarget()); - $this->assertSame(null, $newCommand->getValue()); - } -} diff --git a/tests/Command/CommandRunnerManagerTest.php b/tests/Command/CommandRunnerManagerTest.php deleted file mode 100644 index 5dcb20c0..00000000 --- a/tests/Command/CommandRunnerManagerTest.php +++ /dev/null @@ -1,113 +0,0 @@ -runners = [ - $runner1 = $this->createMock(CommandRunner::class), - $runner2 = $this->createMock(CommandRunner::class), - ]; - $this->preprocessor = $this->createMock(CommandPreprocessorInterface::class); - $this->manager = new CommandRunnerManager($this->runners, $this->preprocessor); - } - - public function testGetAllCommands(): void - { - $this->runners[0]->expects($this->once())->method('getAllCommands')->willReturn([ - 'Action 1' => 'action1', - 'Action 2' => 'action2', - ]); - $this->runners[1]->expects($this->once())->method('getAllCommands')->willReturn([ - 'Action 3' => 'action3', - ]); - $this->assertSame([ - 'Action 1' => 'action1', - 'Action 2' => 'action2', - 'Action 3' => 'action3', - ], $this->manager->getAllCommands()); - } - - public function testGetCommandsRequireTarget(): void - { - $this->runners[0]->expects($this->once())->method('getCommandsRequireTarget')->willReturn([ - 'Action 1' => 'action1', - ]); - $this->runners[1]->expects($this->once())->method('getCommandsRequireTarget')->willReturn([ - 'Action 2' => 'action2', - 'Action 3' => 'action3', - ]); - $this->assertSame([ - 'Action 1' => 'action1', - 'Action 2' => 'action2', - 'Action 3' => 'action3', - ], $this->manager->getCommandsRequireTarget()); - } - - public function testGetCommandsRequireValue(): void - { - $this->runners[0]->expects($this->once())->method('getCommandsRequireValue')->willReturn([ - 'Action 1' => 'action1', - 'Action 4' => 'action4', - ]); - $this->runners[1]->expects($this->once())->method('getCommandsRequireValue')->willReturn([ - 'Action 2' => 'action2', - 'Action 3' => 'action3', - ]); - $this->assertSame([ - 'Action 1' => 'action1', - 'Action 4' => 'action4', - 'Action 2' => 'action2', - 'Action 3' => 'action3', - ], $this->manager->getCommandsRequireValue()); - } - - public function testRunCommandInSecondRunner(): void - { - $command = $this->createMock(CommandInterface::class); - $newCommand = $this->createMock(CommandInterface::class); - $driver = $this->createMock(RemoteWebDriver::class); - $color = $this->createMock(ColorInterface::class); - $this->runners[0]->expects($this->once())->method('supports')->willReturn(false); - $this->runners[0]->expects($this->never())->method('run'); - $this->runners[1]->expects($this->once())->method('supports')->willReturn(true); - $this->runners[1]->expects($this->once())->method('run')->with($newCommand, $color, $driver); - $this->preprocessor->expects($this->once())->method('process')->with($command, $color)->willReturn($newCommand); - $this->manager->run($command, $color, $driver); - } - - public function testRunCommandInFirstRunner(): void - { - $command = $this->createMock(CommandInterface::class); - $newCommand = $this->createMock(CommandInterface::class); - $driver = $this->createMock(RemoteWebDriver::class); - $color = $this->createMock(ColorInterface::class); - $this->runners[0]->expects($this->once())->method('supports')->willReturn(true); - $this->runners[0]->expects($this->once())->method('run')->with($newCommand, $color, $driver); - $this->runners[1]->expects($this->never())->method('supports'); - $this->runners[1]->expects($this->never())->method('run'); - $this->preprocessor->expects($this->once())->method('process')->with($command, $color)->willReturn($newCommand); - $this->manager->run($command, $color, $driver); - } -} diff --git a/tests/Command/CommandTestCase.php b/tests/Command/CommandTestCase.php new file mode 100644 index 00000000..d4f05e56 --- /dev/null +++ b/tests/Command/CommandTestCase.php @@ -0,0 +1,78 @@ +driver = $this->createMock(RemoteWebDriver::class); + $this->command = $this->createCommand(); + $this->values = $this->createMock(ValuesInterface::class); + $this->element = $this->createMock(WebDriverElement::class); + } + + abstract protected function createCommand(): CommandInterface; + + /** + * @dataProvider targetProvider + */ + public function testValidateTarget(?string $target, bool $valid): void + { + $this->assertSame($valid, get_class($this->command)::validateTarget($target)); + } + + abstract public function targetProvider(): array; + + /** + * @dataProvider valueProvider + */ + public function testValidateValue(?string $value, bool $valid): void + { + $this->assertSame($valid, $this->command->validateValue($value)); + } + + abstract public function valueProvider(): array; + + public function testIsTargetRequired(): void + { + $this->assertSame($this->isTargetRequired, get_class($this->command)::isTargetRequired()); + } + + public function testIsValueRequired(): void + { + $this->assertSame($this->isValueRequired, get_class($this->command)::isValueRequired()); + } + + public function testGetTargetHelper(): void + { + $this->assertSame($this->targetHelper, get_class($this->command)::getTargetHelper()); + } + + public function testGetValueHelper(): void + { + $this->assertSame($this->valueHelper, get_class($this->command)::getValueHelper()); + } + + public function testGetGroup(): void + { + $this->assertSame($this->group, get_class($this->command)::getGroup()); + } +} diff --git a/tests/Command/Custom/AssertClipboardCommandTest.php b/tests/Command/Custom/AssertClipboardCommandTest.php new file mode 100644 index 00000000..92efc055 --- /dev/null +++ b/tests/Command/Custom/AssertClipboardCommandTest.php @@ -0,0 +1,98 @@ +httpClient); + } + + /** + * @dataProvider runProvider + */ + public function testRun(ResponseInterface|HttpClientException $response, ?Exception $exception): void + { + $this->setUpUrl(); + $this->driver->expects($this->once())->method('getSessionID')->willReturn($this->sessionId); + $mock = $this->httpClient + ->expects($this->once()) + ->method('request') + ->with( + 'GET', + $this->webdriverUri . '/clipboard/' . $this->sessionId + ); + if ($response instanceof ResponseInterface) { + $mock->willReturn($response); + } else { + $mock->willThrowException($response); + } + if ($exception) { + $this->expectExceptionObject($exception); + } + $this->command->run($this->clipboard, null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [$this->createResponse($this->clipboard), null], + [$this->createResponse('text 2'), new Exception( + "Failed expecting that clipboard's content equals '{$this->clipboard}', actual value 'text 2'" + )], + [ + new HttpClientException('Something wrong'), + new RuntimeException('Can not get clipboard: Something wrong'), + ], + ]; + } + + protected function createResponse(string $clipboard): ResponseInterface + { + $mock = $this->createMock(ResponseInterface::class); + $mock + ->expects($this->once()) + ->method('getContent') + ->willReturn($clipboard); + + return $mock; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Custom/AssertFileDownloadedCommandTest.php b/tests/Command/Custom/AssertFileDownloadedCommandTest.php new file mode 100644 index 00000000..3269832f --- /dev/null +++ b/tests/Command/Custom/AssertFileDownloadedCommandTest.php @@ -0,0 +1,95 @@ +httpClient); + } + + /** + * @dataProvider runProvider + */ + public function testRun(ResponseInterface|HttpClientException $response, ?Exception $exception): void + { + $this->setUpUrl(); + $this->driver->expects($this->once())->method('getSessionID')->willReturn($this->sessionId); + $mock = $this->httpClient + ->expects($this->once()) + ->method('request') + ->with( + 'GET', + $this->webdriverUri . '/download/' . $this->sessionId . '/file.txt' + ); + if ($response instanceof ResponseInterface) { + $mock->willReturn($response); + } else { + $mock->willThrowException($response); + } + if ($exception) { + $this->expectExceptionObject($exception); + } + $this->command->run('file.txt', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [$this->createResponse(200), null], + [$this->createResponse(404), new Exception('Failed expecting that file file.txt is downloaded')], + [ + new HttpClientException('Something wrong'), + new RuntimeException('Can not get downloaded file file.txt: Something wrong'), + ], + ]; + } + + protected function createResponse(int $code): ResponseInterface + { + $mock = $this->createMock(ResponseInterface::class); + $mock + ->expects($this->once()) + ->method('getStatusCode') + ->willReturn($code); + + return $mock; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Custom/HasHttpClientCommandTestCase.php b/tests/Command/Custom/HasHttpClientCommandTestCase.php new file mode 100644 index 00000000..cb9f9c79 --- /dev/null +++ b/tests/Command/Custom/HasHttpClientCommandTestCase.php @@ -0,0 +1,35 @@ +httpClient = $this->createMock(HttpClientInterface::class); + $this->executor = $this->createMock(HttpCommandExecutor::class); + parent::setUp(); + } + + protected function setUpUrl(): void + { + $this->executor + ->expects($this->once()) + ->method('getAddressOfRemoteServer') + ->willReturn($this->webdriverUri . '/wd/hub'); + $this->driver + ->expects($this->once()) + ->method('getCommandExecutor') + ->willReturn($this->executor); + } +} diff --git a/tests/Command/Custom/UpdateClipboardCommandTest.php b/tests/Command/Custom/UpdateClipboardCommandTest.php new file mode 100644 index 00000000..f25c70b7 --- /dev/null +++ b/tests/Command/Custom/UpdateClipboardCommandTest.php @@ -0,0 +1,95 @@ +httpClient); + } + + /** + * @dataProvider runProvider + */ + public function testRun(ResponseInterface|HttpClientException $response, ?Exception $exception): void + { + $this->setUpUrl(); + $this->driver->expects($this->once())->method('getSessionID')->willReturn($this->sessionId); + $mock = $this->httpClient + ->expects($this->once()) + ->method('request') + ->with( + 'POST', + $this->webdriverUri . '/clipboard/' . $this->sessionId, + ['body' => 'clipboard'] + ); + if ($response instanceof ResponseInterface) { + $mock->willReturn($response); + } else { + $mock->willThrowException($response); + } + if ($exception) { + $this->expectExceptionObject($exception); + } + $this->command->run('clipboard', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [$this->createResponse(123), null], + [ + new HttpClientException('Something wrong'), + new RuntimeException('Can not update clipboard: Something wrong'), + ], + ]; + } + + protected function createResponse(int $code): ResponseInterface + { + $mock = $this->createMock(ResponseInterface::class); + $mock + ->expects($this->once()) + ->method('getStatusCode') + ->willReturn($code); + + return $mock; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Custom/UploadCommandTest.php b/tests/Command/Custom/UploadCommandTest.php new file mode 100644 index 00000000..32287fc5 --- /dev/null +++ b/tests/Command/Custom/UploadCommandTest.php @@ -0,0 +1,73 @@ +uploadDir); + } + + public function testRun(): void + { + $this->element = $this->createMock(RemoteWebElement::class); + $this->element + ->expects($this->once()) + ->method('setFileDetector') + ->with($this->isInstanceOf(LocalFileDetector::class)) + ->willReturnSelf(); + $this->element + ->expects($this->once()) + ->method('sendKeys') + ->with($this->uploadDir . '/sub-directory/file.txt'); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'file_input' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('id=file_input', 'sub-directory/file.txt', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['video.mp4', true], + ]; + } +} diff --git a/tests/Command/Keyboard/SendKeysCommandTest.php b/tests/Command/Keyboard/SendKeysCommandTest.php new file mode 100644 index 00000000..6c6991fb --- /dev/null +++ b/tests/Command/Keyboard/SendKeysCommandTest.php @@ -0,0 +1,57 @@ +element->expects($this->once())->method('click')->willReturnSelf(); + $this->element->expects($this->once())->method('sendKeys')->with('123'); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.quantity' === $selector->getValue(); + }))->willReturn($this->element); + $this->command->run('css=.quantity', '123', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Keyboard/TypeCommandTest.php b/tests/Command/Keyboard/TypeCommandTest.php new file mode 100644 index 00000000..01fe80af --- /dev/null +++ b/tests/Command/Keyboard/TypeCommandTest.php @@ -0,0 +1,62 @@ +element->expects($this->once())->method('click')->willReturnSelf(); + $this->element->expects($this->once())->method('clear')->willReturnSelf(); + $this->element->expects($this->once())->method('sendKeys')->with('20 years old'); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'name' === $selector->getMechanism() + && 'age' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('name=age', '20 years old', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Mouse/AddSelectionCommandTest.php b/tests/Command/Mouse/AddSelectionCommandTest.php new file mode 100644 index 00000000..a3bfdc70 --- /dev/null +++ b/tests/Command/Mouse/AddSelectionCommandTest.php @@ -0,0 +1,82 @@ +driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'partial link text' === $selector->getMechanism() + && 'Language' === $selector->getValue(); + })) + ->willReturn($this->element); + $select = $this->createMock(WebDriverSelect::class); + $select->expects($this->once())->method($method)->with($with); + $command = $this->createPartialMock(AddSelectionCommand::class, ['getSelect']); + $command->expects($this->once())->method('getSelect')->with($this->element)->willReturn($select); + $command->run('partialLinkText=Language', $value, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['index=123', 'selectByIndex', 123], + ['value=en_GB', 'selectByValue', 'en_GB'], + ['label=English (UK)', 'selectByVisibleText', 'English (UK)'], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['=', false], + ['key=value', false], + ['index=123', true], + ['value=abc123', true], + ['label=Text', true], + ]; + } +} diff --git a/tests/Command/Mouse/CheckCommandTest.php b/tests/Command/Mouse/CheckCommandTest.php new file mode 100644 index 00000000..b15f8f41 --- /dev/null +++ b/tests/Command/Mouse/CheckCommandTest.php @@ -0,0 +1,72 @@ +element->expects($this->once())->method('isSelected')->willReturn($selected); + $this->element->expects($this->exactly(+$checked))->method('click'); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'language' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('id=language', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [true, false], + [false, true], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Mouse/ClickAtCommandTest.php b/tests/Command/Mouse/ClickAtCommandTest.php new file mode 100644 index 00000000..3ed31924 --- /dev/null +++ b/tests/Command/Mouse/ClickAtCommandTest.php @@ -0,0 +1,66 @@ +driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($this->element); + $action = $this->createMock(WebDriverActions::class); + $action->expects($this->once())->method('moveToElement')->with($this->element, 5, 10)->willReturnSelf(); + $action->expects($this->once())->method('click')->willReturnSelf(); + $action->expects($this->once())->method('perform'); + $this->driver->expects($this->once())->method('action')->willReturn($action); + $this->command->run('id=cart', '5,10', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['123', false], + ['123,', false], + [',123', false], + ['123,456', true], + ]; + } +} diff --git a/tests/Command/Mouse/ClickCommandTest.php b/tests/Command/Mouse/ClickCommandTest.php new file mode 100644 index 00000000..083e32ec --- /dev/null +++ b/tests/Command/Mouse/ClickCommandTest.php @@ -0,0 +1,56 @@ +element->expects($this->once())->method('click'); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'add-to-cart' === $selector->getValue(); + }))->willReturn($this->element); + $this->command->run('id=add-to-cart', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Mouse/DoubleClickAtCommandTest.php b/tests/Command/Mouse/DoubleClickAtCommandTest.php new file mode 100644 index 00000000..c71396f4 --- /dev/null +++ b/tests/Command/Mouse/DoubleClickAtCommandTest.php @@ -0,0 +1,66 @@ +driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($this->element); + $action = $this->createMock(WebDriverActions::class); + $action->expects($this->once())->method('moveToElement')->with($this->element, 5, 10)->willReturnSelf(); + $action->expects($this->once())->method('doubleClick')->willReturnSelf(); + $action->expects($this->once())->method('perform'); + $this->driver->expects($this->once())->method('action')->willReturn($action); + $this->command->run('id=cart', '5,10', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['123', false], + ['123,', false], + [',123', false], + ['123,456', true], + ]; + } +} diff --git a/tests/Command/Mouse/DoubleClickCommandTest.php b/tests/Command/Mouse/DoubleClickCommandTest.php new file mode 100644 index 00000000..60ac8621 --- /dev/null +++ b/tests/Command/Mouse/DoubleClickCommandTest.php @@ -0,0 +1,60 @@ +driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($this->element); + $action = $this->createMock(WebDriverActions::class); + $action->expects($this->once())->method('doubleClick')->with($this->element)->willReturnSelf(); + $action->expects($this->once())->method('perform'); + $this->driver->expects($this->once())->method('action')->willReturn($action); + $this->command->run('id=cart', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Mouse/DragAndDropToObjectCommandTest.php b/tests/Command/Mouse/DragAndDropToObjectCommandTest.php new file mode 100644 index 00000000..3478dc56 --- /dev/null +++ b/tests/Command/Mouse/DragAndDropToObjectCommandTest.php @@ -0,0 +1,78 @@ +createMock(WebDriverElement::class); + $target = $this->createMock(WebDriverElement::class); + $this->driver + ->expects($this->exactly(2)) + ->method('findElement') + ->withConsecutive( + [$this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'product' === $selector->getValue(); + })], + [$this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + })], + )->willReturnOnConsecutiveCalls($source, $target); + $action = $this->createMock(WebDriverActions::class); + $action + ->expects($this->once()) + ->method('dragAndDrop') + ->with($source, $target) + ->willReturnSelf(); + $action->expects($this->once())->method('perform'); + $this->driver->expects($this->once())->method('action')->willReturn($action); + $this->command->run('id=product', 'id=cart', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } +} diff --git a/tests/Command/Mouse/MouseDownAtCommandTest.php b/tests/Command/Mouse/MouseDownAtCommandTest.php new file mode 100644 index 00000000..2262b3a0 --- /dev/null +++ b/tests/Command/Mouse/MouseDownAtCommandTest.php @@ -0,0 +1,70 @@ +createMock(WebDriverCoordinates::class); + $element = $this->createMock(RemoteWebElement::class); + $element->expects($this->once())->method('getCoordinates')->willReturn($coord); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($element); + $mouse = $this->createMock(RemoteMouse::class); + $mouse->expects($this->once())->method('mouseMove')->with($coord, 5, 10)->willReturnSelf(); + $mouse->expects($this->once())->method('mouseDown'); + $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); + $this->command->run('id=cart', '5,10', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['123', false], + ['123,', false], + [',123', false], + ['123,456', true], + ]; + } +} diff --git a/tests/Command/Mouse/MouseDownCommandTest.php b/tests/Command/Mouse/MouseDownCommandTest.php new file mode 100644 index 00000000..904e51c6 --- /dev/null +++ b/tests/Command/Mouse/MouseDownCommandTest.php @@ -0,0 +1,64 @@ +createMock(WebDriverCoordinates::class); + $element = $this->createMock(RemoteWebElement::class); + $element->expects($this->once())->method('getCoordinates')->willReturn($coord); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($element); + $mouse = $this->createMock(RemoteMouse::class); + $mouse->expects($this->once())->method('mouseDown')->with($coord); + $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); + $this->command->run('id=cart', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Mouse/MouseMoveAtCommandTest.php b/tests/Command/Mouse/MouseMoveAtCommandTest.php new file mode 100644 index 00000000..b0f2202b --- /dev/null +++ b/tests/Command/Mouse/MouseMoveAtCommandTest.php @@ -0,0 +1,69 @@ +createMock(WebDriverCoordinates::class); + $element = $this->createMock(RemoteWebElement::class); + $element->expects($this->once())->method('getCoordinates')->willReturn($coord); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($element); + $mouse = $this->createMock(RemoteMouse::class); + $mouse->expects($this->once())->method('mouseMove')->with($coord, 5, 10); + $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); + $this->command->run('id=cart', '5,10', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['123', false], + ['123,', false], + [',123', false], + ['123,456', true], + ]; + } +} diff --git a/tests/Command/Mouse/MouseOutCommandTest.php b/tests/Command/Mouse/MouseOutCommandTest.php new file mode 100644 index 00000000..f57af909 --- /dev/null +++ b/tests/Command/Mouse/MouseOutCommandTest.php @@ -0,0 +1,118 @@ +createMock(RemoteWebElement::class); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($element); + if ($exception) { + $this->driver->expects($this->never())->method('getMouse'); + $this->expectExceptionObject($exception); + } else { + $coord = $this->createMock(WebDriverCoordinates::class); + $element->expects($this->once())->method('getCoordinates')->willReturn($coord); + $mouse = $this->createMock(RemoteMouse::class); + $mouse->expects($this->once())->method('mouseMove')->with($coord, $x, $y); + $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); + } + $this->driver->expects($this->once())->method('executeScript')->with( + 'return [arguments[0].getBoundingClientRect(), {height: window.innerHeight, width: window.innerWidth}];', + [$element] + )->willReturn([$rect, $vp]); + $this->command->run('id=cart', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [ + 'rect' => (object) ['top' => 1, 'height' => 2], + 'vp' => (object) [], + 'x' => null, + 'y' => -2, + 'exception' => null, + ], + [ + 'rect' => (object) ['top' => 0, 'right' => 2], + 'vp' => (object) ['width' => 3], + 'x' => 2, + 'y' => null, + 'exception' => null, + ], + [ + 'rect' => (object) ['top' => 0, 'right' => 2, 'bottom' => 1, 'height' => 4], + 'vp' => (object) ['width' => 2, 'height' => 2], + 'x' => null, + 'y' => 3, + 'exception' => null, + ], + [ + 'rect' => (object) ['top' => 0, 'right' => 2, 'bottom' => 1, 'left' => 1], + 'vp' => (object) ['width' => 2, 'height' => 1], + 'x' => -1, + 'y' => null, + 'exception' => null, + ], + [ + 'rect' => (object) ['top' => 0, 'right' => 2, 'bottom' => 1, 'left' => 0], + 'vp' => (object) ['width' => 2, 'height' => 1], + 'x' => null, + 'y' => null, + 'exception' => new Exception('Unable to perform mouse out as the element takes up the entire viewport'), + ], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Mouse/MouseOverCommandTest.php b/tests/Command/Mouse/MouseOverCommandTest.php new file mode 100644 index 00000000..6002b26f --- /dev/null +++ b/tests/Command/Mouse/MouseOverCommandTest.php @@ -0,0 +1,64 @@ +createMock(WebDriverCoordinates::class); + $element = $this->createMock(RemoteWebElement::class); + $element->expects($this->once())->method('getCoordinates')->willReturn($coord); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($element); + $mouse = $this->createMock(RemoteMouse::class); + $mouse->expects($this->once())->method('mouseMove')->with($coord); + $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); + $this->command->run('id=cart', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Mouse/MouseUpAtCommandTest.php b/tests/Command/Mouse/MouseUpAtCommandTest.php new file mode 100644 index 00000000..b439f4bd --- /dev/null +++ b/tests/Command/Mouse/MouseUpAtCommandTest.php @@ -0,0 +1,70 @@ +createMock(WebDriverCoordinates::class); + $element = $this->createMock(RemoteWebElement::class); + $element->expects($this->once())->method('getCoordinates')->willReturn($coord); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($element); + $mouse = $this->createMock(RemoteMouse::class); + $mouse->expects($this->once())->method('mouseMove')->with($coord, 5, 10)->willReturnSelf(); + $mouse->expects($this->once())->method('mouseUp'); + $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); + $this->command->run('id=cart', '5,10', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['123', false], + ['123,', false], + [',123', false], + ['123,456', true], + ]; + } +} diff --git a/tests/Command/Mouse/MouseUpCommandTest.php b/tests/Command/Mouse/MouseUpCommandTest.php new file mode 100644 index 00000000..7d067496 --- /dev/null +++ b/tests/Command/Mouse/MouseUpCommandTest.php @@ -0,0 +1,64 @@ +createMock(WebDriverCoordinates::class); + $element = $this->createMock(RemoteWebElement::class); + $element->expects($this->once())->method('getCoordinates')->willReturn($coord); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'cart' === $selector->getValue(); + }))->willReturn($element); + $mouse = $this->createMock(RemoteMouse::class); + $mouse->expects($this->once())->method('mouseUp')->with($coord); + $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); + $this->command->run('id=cart', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Mouse/RemoveSelectionCommandTest.php b/tests/Command/Mouse/RemoveSelectionCommandTest.php new file mode 100644 index 00000000..de14048d --- /dev/null +++ b/tests/Command/Mouse/RemoveSelectionCommandTest.php @@ -0,0 +1,82 @@ +driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'partial link text' === $selector->getMechanism() + && 'Language' === $selector->getValue(); + })) + ->willReturn($this->element); + $select = $this->createMock(WebDriverSelect::class); + $select->expects($this->once())->method($method)->with($with); + $command = $this->createPartialMock(RemoveSelectionCommand::class, ['getSelect']); + $command->expects($this->once())->method('getSelect')->with($this->element)->willReturn($select); + $command->run('partialLinkText=Language', $value, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['index=123', 'deselectByIndex', 123], + ['value=en_GB', 'deselectByValue', 'en_GB'], + ['label=English (UK)', 'deselectByVisibleText', 'English (UK)'], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['=', false], + ['key=value', false], + ['index=123', true], + ['value=abc123', true], + ['label=Text', true], + ]; + } +} diff --git a/tests/Command/Mouse/SelectCommandTest.php b/tests/Command/Mouse/SelectCommandTest.php new file mode 100644 index 00000000..af05b1b3 --- /dev/null +++ b/tests/Command/Mouse/SelectCommandTest.php @@ -0,0 +1,65 @@ +createMock(WebDriverElement::class); + $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'partial link text' === $selector->getMechanism() + && 'Language' === $selector->getValue(); + }))->willReturn($select); + $option = $this->createMock(WebDriverElement::class); + $option->expects($this->once())->method('click'); + $select->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && 'option[value=en_US]' === $selector->getValue(); + }))->willReturn($option); + $this->command->run('partialLinkText=Language', 'css=option[value=en_US]', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } +} diff --git a/tests/Command/Mouse/UncheckCommandTest.php b/tests/Command/Mouse/UncheckCommandTest.php new file mode 100644 index 00000000..6c5fb551 --- /dev/null +++ b/tests/Command/Mouse/UncheckCommandTest.php @@ -0,0 +1,72 @@ +element->expects($this->once())->method('isSelected')->willReturn($selected); + $this->element->expects($this->exactly(+$unchecked))->method('click'); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'language' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('id=language', null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [true, true], + [false, false], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Runner/AlertCommandRunnerTest.php b/tests/Command/Runner/AlertCommandRunnerTest.php deleted file mode 100644 index 2fe12f93..00000000 --- a/tests/Command/Runner/AlertCommandRunnerTest.php +++ /dev/null @@ -1,90 +0,0 @@ -setCommand($acceptAlertCommand); - $alert = $this->createMock(WebDriverAlert::class); - $alert->expects($this->once())->method('accept'); - $targetLocator = $this->createMock(RemoteTargetLocator::class); - $targetLocator->expects($this->once())->method('alert')->willReturn($alert); - $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); - $this->runner->run($command, $this->color, $this->driver); - } - - /** - * @dataProvider dismissAlertCommandProvider - */ - public function testDismissAlert(string $acceptAlertCommand): void - { - $command = new Command(); - $command->setCommand($acceptAlertCommand); - $alert = $this->createMock(WebDriverAlert::class); - $alert->expects($this->once())->method('dismiss'); - $targetLocator = $this->createMock(RemoteTargetLocator::class); - $targetLocator->expects($this->once())->method('alert')->willReturn($alert); - $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAnswerPrompt(): void - { - $command = new Command(); - $command->setCommand(AlertCommandRunner::ANSWER_PROMPT); - $command->setTarget('Yes, I agree'); - $alert = $this->createMock(WebDriverAlert::class); - $alert->expects($this->once())->method('sendKeys')->with('Yes, I agree'); - $alert->expects($this->once())->method('accept'); - $targetLocator = $this->createMock(RemoteTargetLocator::class); - $targetLocator->expects($this->once())->method('alert')->willReturn($alert); - $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); - $this->runner->run($command, $this->color, $this->driver); - } - - public function acceptAlertCommandProvider(): array - { - return [ - [AlertCommandRunner::ACCEPT_ALERT], - [AlertCommandRunner::ACCEPT_CONFIRMATION], - ]; - } - - public function dismissAlertCommandProvider(): array - { - return [ - [AlertCommandRunner::DISMISS_CONFIRMATION], - [AlertCommandRunner::DISMISS_PROMPT], - ]; - } - - public function targetProvider(): array - { - return [ - [AlertCommandRunner::ANSWER_PROMPT, 'anything', true], - ]; - } -} diff --git a/tests/Command/Runner/AssertionRunnerTest.php b/tests/Command/Runner/AssertionRunnerTest.php deleted file mode 100644 index 188d514f..00000000 --- a/tests/Command/Runner/AssertionRunnerTest.php +++ /dev/null @@ -1,605 +0,0 @@ -setCommand(AssertionRunner::ASSERT); - $command->setTarget('var name'); - $command->setValue('var value'); - $this->color->expects($this->once())->method('getValue')->with('var name')->willReturn('var value'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertFailed(): void - { - $expected = 'var value'; - $actual = 'var value 1'; - $this->expectException(\Exception::class); - $this->expectExceptionMessage(sprintf('Actual value "%s" did not match "%s"', $actual, $expected)); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT); - $command->setTarget('var name'); - $command->setValue($expected); - $this->color->expects($this->once())->method('getValue')->with('var name')->willReturn($actual); - $this->runner->run($command, $this->color, $this->driver); - } - - /** - * @dataProvider alertCommandProvider - */ - public function testAssertAlertPassed(string $alertCommand): void - { - $command = new Command(); - $command->setCommand($alertCommand); - $command->setTarget('Are you sure you want to close this window?'); - $alert = $this->createMock(WebDriverAlert::class); - $alert->expects($this->once())->method('getText')->willReturn('Are you sure you want to close this window?'); - $locator = $this->createMock(RemoteTargetLocator::class); - $locator->expects($this->once())->method('alert')->willReturn($alert); - $this->driver->expects($this->once())->method('switchTo')->willReturn($locator); - $this->runner->run($command, $this->color, $this->driver); - } - - /** - * @dataProvider alertCommandProvider - */ - public function testAssertAlertFailed(string $alertCommand, string $type): void - { - $expected = 'Are you sure you want to close this window?'; - $actual = 'Dont close this window!'; - $this->expectException(\Exception::class); - $this->expectExceptionMessage(sprintf('Actual %s text "%s" did not match "%s"', $type, $actual, $expected)); - $command = new Command(); - $command->setCommand($alertCommand); - $command->setTarget($expected); - $alert = $this->createMock(WebDriverAlert::class); - $alert->expects($this->once())->method('getText')->willReturn($actual); - $locator = $this->createMock(RemoteTargetLocator::class); - $locator->expects($this->once())->method('alert')->willReturn($alert); - $this->driver->expects($this->once())->method('switchTo')->willReturn($locator); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertTitlePassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_TITLE); - $command->setTarget('Welcome'); - $this->driver->expects($this->exactly(2))->method('getTitle')->willReturn('Welcome'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertTitleFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Actual title "Goodbye" did not match "Welcome"'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_TITLE); - $command->setTarget('Welcome'); - $this->driver->expects($this->exactly(2))->method('getTitle')->willReturn('Goodbye'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertTextPassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_TEXT); - $command->setTarget('xpath=//h4[@href="#"]'); - $command->setValue('Welcome to our store'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getText')->willReturn('Welcome to our store'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'xpath' === $selector->getMechanism() - && '//h4[@href="#"]' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertTextFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Actual text "Goodbye! See you again" did not match "Welcome to our store"'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_TEXT); - $command->setTarget('xpath=//h4[@href="#"]'); - $command->setValue('Welcome to our store'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getText')->willReturn('Goodbye! See you again'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'xpath' === $selector->getMechanism() - && '//h4[@href="#"]' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotTextFailed(): void - { - $expected = 'Welcome to our store'; - $this->expectException(\Exception::class); - $this->expectExceptionMessage(sprintf('Actual text "%s" did match "%s"', $expected, $expected)); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_TEXT); - $command->setTarget('xpath=//h4[@href="#"]'); - $command->setValue($expected); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getText')->willReturn($expected); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'xpath' === $selector->getMechanism() - && '//h4[@href="#"]' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotTextPassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_TEXT); - $command->setTarget('xpath=//h4[@href="#"]'); - $command->setValue('Welcome to our store'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getText')->willReturn('Goodbye! See you again'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'xpath' === $selector->getMechanism() - && '//h4[@href="#"]' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertValuePassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_VALUE); - $command->setTarget('css=.quality'); - $command->setValue('14'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getAttribute')->with('value')->willReturn('14'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.quality' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertValueFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Actual value "15" did not match "14"'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_VALUE); - $command->setTarget('css=.quality'); - $command->setValue('14'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getAttribute')->with('value')->willReturn('15'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.quality' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertEditablePassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_EDITABLE); - $command->setTarget('name=username'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'name' === $selector->getMechanism() - && 'username' === $selector->getValue(); - }))->willReturn($element); - $this->driver - ->expects($this->once()) - ->method('executeScript') - ->with('return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', [$element]) - ->willReturn((object) ['enabled' => true, 'readonly' => false]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertEditableFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Element "name=username" is not editable'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_EDITABLE); - $command->setTarget('name=username'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'name' === $selector->getMechanism() - && 'username' === $selector->getValue(); - }))->willReturn($element); - $this->driver - ->expects($this->once()) - ->method('executeScript') - ->with('return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', [$element]) - ->willReturn((object) ['enabled' => false, 'readonly' => true]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotEditableFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Element "name=username" is editable'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_EDITABLE); - $command->setTarget('name=username'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'name' === $selector->getMechanism() - && 'username' === $selector->getValue(); - }))->willReturn($element); - $this->driver - ->expects($this->once()) - ->method('executeScript') - ->with('return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', [$element]) - ->willReturn((object) ['enabled' => true, 'readonly' => false]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotEditablePassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_EDITABLE); - $command->setTarget('name=username'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'name' === $selector->getMechanism() - && 'username' === $selector->getValue(); - }))->willReturn($element); - $this->driver - ->expects($this->once()) - ->method('executeScript') - ->with('return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', [$element]) - ->willReturn((object) ['enabled' => false, 'readonly' => true]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertElementPresentPassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_ELEMENT_PRESENT); - $command->setTarget('css=.cart'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElements')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.cart' === $selector->getValue(); - }))->willReturn([$element]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertElementPresentFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Expected element "css=.cart" was not found in page'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_ELEMENT_PRESENT); - $command->setTarget('css=.cart'); - $this->driver->expects($this->once())->method('findElements')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.cart' === $selector->getValue(); - }))->willReturn([]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertElementNotPresentFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Unexpected element "css=.cart" was found in page'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_ELEMENT_NOT_PRESENT); - $command->setTarget('css=.cart'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElements')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.cart' === $selector->getValue(); - }))->willReturn([$element]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertElementNotPresentPassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_ELEMENT_NOT_PRESENT); - $command->setTarget('css=.cart'); - $this->driver->expects($this->once())->method('findElements')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.cart' === $selector->getValue(); - }))->willReturn([]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertCheckedPassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_CHECKED); - $command->setTarget('css=.term-and-condition'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('isSelected')->willReturn(true); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.term-and-condition' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertCheckedFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Element "css=.term-and-condition" is not checked, expected to be checked'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_CHECKED); - $command->setTarget('css=.term-and-condition'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('isSelected')->willReturn(false); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.term-and-condition' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotCheckedFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Element "css=.term-and-condition" is checked, expected to be unchecked'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_CHECKED); - $command->setTarget('css=.term-and-condition'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('isSelected')->willReturn(true); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.term-and-condition' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotCheckedPassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_CHECKED); - $command->setTarget('css=.term-and-condition'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('isSelected')->willReturn(false); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.term-and-condition' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testAssertSelectedValuePassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_SELECTED_VALUE); - $command->setTarget('partialLinkText=Language'); - $command->setValue('en_GB'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('getAttribute')->with('value')->willReturn('en_GB'); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); - $runner = $this->createPartialMock(AssertionRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAssertSelectedValueFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Actual value "en_US" did not match "en_GB"'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_SELECTED_VALUE); - $command->setTarget('partialLinkText=Language'); - $command->setValue('en_GB'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('getAttribute')->with('value')->willReturn('en_US'); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); - $runner = $this->createPartialMock(AssertionRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotSelectedValueFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Actual value "en_GB" did match "en_GB"'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_SELECTED_VALUE); - $command->setTarget('partialLinkText=Language'); - $command->setValue('en_GB'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('getAttribute')->with('value')->willReturn('en_GB'); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); - $runner = $this->createPartialMock(AssertionRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotSelectedValuePassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_SELECTED_VALUE); - $command->setTarget('partialLinkText=Language'); - $command->setValue('en_GB'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('getAttribute')->with('value')->willReturn('en_US'); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); - $runner = $this->createPartialMock(AssertionRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAssertSelectedLabelPassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_SELECTED_LABEL); - $command->setTarget('partialLinkText=Language'); - $command->setValue('United Kingdom'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('getText')->willReturn('United Kingdom'); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); - $runner = $this->createPartialMock(AssertionRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAssertSelectedLabelFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Actual label "United States" did not match "United Kingdom"'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_SELECTED_LABEL); - $command->setTarget('partialLinkText=Language'); - $command->setValue('United Kingdom'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('getText')->willReturn('United States'); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); - $runner = $this->createPartialMock(AssertionRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotSelectedLabelFailed(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Actual label "United Kingdom" did match "United Kingdom"'); - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_SELECTED_LABEL); - $command->setTarget('partialLinkText=Language'); - $command->setValue('United Kingdom'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('getText')->willReturn('United Kingdom'); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); - $runner = $this->createPartialMock(AssertionRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAssertNotSelectedLabelPassed(): void - { - $command = new Command(); - $command->setCommand(AssertionRunner::ASSERT_NOT_SELECTED_LABEL); - $command->setTarget('partialLinkText=Language'); - $command->setValue('United Kingdom'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('getText')->willReturn('United States'); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('getFirstSelectedOption')->willReturn($option); - $runner = $this->createPartialMock(AssertionRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function alertCommandProvider(): array - { - return [ - [AssertionRunner::ASSERT_ALERT, 'alert'], - [AssertionRunner::ASSERT_CONFIRMATION, 'confirm'], - [AssertionRunner::ASSERT_PROMPT, 'prompt'], - ]; - } - - public function targetProvider(): array - { - return [ - [AssertionRunner::ASSERT_TEXT, 'invalid=anything', false], - [AssertionRunner::ASSERT_VALUE, 'css=.age', true], - [AssertionRunner::ASSERT, 'anything', true], - ]; - } -} diff --git a/tests/Command/Runner/KeyboardCommandRunnerTest.php b/tests/Command/Runner/KeyboardCommandRunnerTest.php deleted file mode 100644 index f8f9c84b..00000000 --- a/tests/Command/Runner/KeyboardCommandRunnerTest.php +++ /dev/null @@ -1,67 +0,0 @@ -setCommand(KeyboardCommandRunner::TYPE); - $command->setTarget('name=age'); - $command->setValue('20 years old'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('click')->willReturnSelf(); - $element->expects($this->once())->method('clear')->willReturnSelf(); - $element->expects($this->once())->method('sendKeys')->with(['20 years old']); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'name' === $selector->getMechanism() - && 'age' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testSendKeys(): void - { - $command = new Command(); - $command->setCommand(KeyboardCommandRunner::SEND_KEYS); - $command->setTarget('css=.quantity'); - $command->setValue('123'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('click')->willReturnSelf(); - $element->expects($this->once())->method('sendKeys')->with(['123']); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.quantity' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function targetProvider(): array - { - return [ - [KeyboardCommandRunner::SEND_KEYS, null, false], - [KeyboardCommandRunner::SEND_KEYS, 'anything', false], - [KeyboardCommandRunner::SEND_KEYS, 'xpath=//path/to/element', true], - ]; - } -} diff --git a/tests/Command/Runner/MouseCommandRunnerTest.php b/tests/Command/Runner/MouseCommandRunnerTest.php deleted file mode 100644 index 8f591c5f..00000000 --- a/tests/Command/Runner/MouseCommandRunnerTest.php +++ /dev/null @@ -1,578 +0,0 @@ -setCommand(MouseCommandRunner::ADD_SELECTION); - $command->setTarget('partialLinkText=Language'); - $command->setValue('index=123'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('selectByIndex')->with(123); - $runner = $this->createPartialMock(MouseCommandRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAddSelectionByValue(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::ADD_SELECTION); - $command->setTarget('partialLinkText=Language'); - $command->setValue('value=en_GB'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('selectByValue')->with('en_GB'); - $runner = $this->createPartialMock(MouseCommandRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testAddSelectionByLabel(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::ADD_SELECTION); - $command->setTarget('partialLinkText=Language'); - $command->setValue('label=English (UK)'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('selectByVisibleText')->with('English (UK)'); - $runner = $this->createPartialMock(MouseCommandRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testRemoveSelectionByIndex(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::REMOVE_SELECTION); - $command->setTarget('partialLinkText=Language'); - $command->setValue('index=123'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('deselectByIndex')->with(123); - $runner = $this->createPartialMock(MouseCommandRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testRemoveSelectionByValue(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::REMOVE_SELECTION); - $command->setTarget('partialLinkText=Language'); - $command->setValue('value=en_GB'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('deselectByValue')->with('en_GB'); - $runner = $this->createPartialMock(MouseCommandRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - public function testRemoveSelectionByLabel(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::REMOVE_SELECTION); - $command->setTarget('partialLinkText=Language'); - $command->setValue('label=English (UK)'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($element); - $select = $this->createMock(WebDriverSelect::class); - $select->expects($this->once())->method('deselectByVisibleText')->with('English (UK)'); - $runner = $this->createPartialMock(MouseCommandRunner::class, ['getSelect']); - $runner->expects($this->once())->method('getSelect')->with($element)->willReturn($select); - $runner->run($command, $this->color, $this->driver); - } - - /** - * @dataProvider checkDataProvider - */ - public function testCheck(bool $selected, bool $checked): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::CHECK); - $command->setTarget('id=language'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('isSelected')->willReturn($selected); - $element->expects($this->exactly(+$checked))->method('click'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'language' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - /** - * @dataProvider uncheckDataProvider - */ - public function testUncheck(bool $selected, bool $unchecked): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::UNCHECK); - $command->setTarget('id=language'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('isSelected')->willReturn($selected); - $element->expects($this->exactly(+$unchecked))->method('click'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'language' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testClick(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::CLICK); - $command->setTarget('id=add-to-cart'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('click'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'add-to-cart' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testClickAt(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::CLICK_AT); - $command->setTarget('id=cart'); - $command->setValue('5,10'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $action = $this->createMock(WebDriverActions::class); - $action->expects($this->once())->method('moveToElement')->with($element, 5, 10)->willReturnSelf(); - $action->expects($this->once())->method('click')->willReturnSelf(); - $action->expects($this->once())->method('perform'); - $this->driver->expects($this->once())->method('action')->willReturn($action); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testDoubleClick(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::DOUBLE_CLICK); - $command->setTarget('id=cart'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $action = $this->createMock(WebDriverActions::class); - $action->expects($this->once())->method('doubleClick')->with($element)->willReturnSelf(); - $action->expects($this->once())->method('perform'); - $this->driver->expects($this->once())->method('action')->willReturn($action); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testDoubleClickAt(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::DOUBLE_CLICK_AT); - $command->setTarget('id=cart'); - $command->setValue('5,10'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $action = $this->createMock(WebDriverActions::class); - $action->expects($this->once())->method('moveToElement')->with($element, 5, 10)->willReturnSelf(); - $action->expects($this->once())->method('doubleClick')->willReturnSelf(); - $action->expects($this->once())->method('perform'); - $this->driver->expects($this->once())->method('action')->willReturn($action); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testDragAndDropToObject(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::DRAG_AND_DROP_TO_OBJECT); - $command->setTarget('id=product'); - $command->setValue('id=cart'); - $source = $this->createMock(WebDriverElement::class); - $target = $this->createMock(WebDriverElement::class); - $this->driver - ->expects($this->exactly(2)) - ->method('findElement') - ->withConsecutive( - [$this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'product' === $selector->getValue(); - })], - [$this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - })], - )->willReturnOnConsecutiveCalls($source, $target); - $action = $this->createMock(WebDriverActions::class); - $action->expects($this->once())->method('dragAndDrop')->with($source, $target)->willReturnSelf(); - $action->expects($this->once())->method('perform'); - $this->driver->expects($this->once())->method('action')->willReturn($action); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseDown(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_DOWN); - $command->setTarget('id=cart'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseDown')->with($coord); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseDownAt(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_DOWN_AT); - $command->setTarget('id=cart'); - $command->setValue('5,10'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseMove')->with($coord, 5, 10)->willReturnSelf(); - $mouse->expects($this->once())->method('mouseDown'); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseMoveAt(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_MOVE_AT); - $command->setTarget('id=cart'); - $command->setValue('5,10'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseMove')->with($coord, 5, 10); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseOutTop(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_OUT); - $command->setTarget('id=cart'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseMove')->with($coord, null, -2); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->driver->expects($this->once())->method('executeScript')->with( - 'return [arguments[0].getBoundingClientRect(), {height: window.innerHeight, width: window.innerWidth}];', - [$element] - )->willReturn([ - $rect = (object) ['top' => 1, 'height' => 2], - $vp = (object) [], - ]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseOutRight(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_OUT); - $command->setTarget('id=cart'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseMove')->with($coord, 2); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->driver->expects($this->once())->method('executeScript')->with( - 'return [arguments[0].getBoundingClientRect(), {height: window.innerHeight, width: window.innerWidth}];', - [$element] - )->willReturn([ - $rect = (object) ['top' => 0, 'right' => 2], - $vp = (object) ['width' => 3], - ]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseOutBottom(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_OUT); - $command->setTarget('id=cart'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseMove')->with($coord, null, 3); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->driver->expects($this->once())->method('executeScript')->with( - 'return [arguments[0].getBoundingClientRect(), {height: window.innerHeight, width: window.innerWidth}];', - [$element] - )->willReturn([ - $rect = (object) ['top' => 0, 'right' => 2, 'bottom' => 1, 'height' => 4], - $vp = (object) ['width' => 2, 'height' => 2], - ]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseOutLeft(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_OUT); - $command->setTarget('id=cart'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseMove')->with($coord, -1); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->driver->expects($this->once())->method('executeScript')->with( - 'return [arguments[0].getBoundingClientRect(), {height: window.innerHeight, width: window.innerWidth}];', - [$element] - )->willReturn([ - $rect = (object) ['top' => 0, 'right' => 2, 'bottom' => 1, 'left' => 1], - $vp = (object) ['width' => 2, 'height' => 1], - ]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testUnableMouseOut(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_OUT); - $command->setTarget('id=cart'); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->never())->method('getCoordinates'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $this->driver->expects($this->never())->method('getMouse'); - $this->driver->expects($this->once())->method('executeScript')->with( - 'return [arguments[0].getBoundingClientRect(), {height: window.innerHeight, width: window.innerWidth}];', - [$element] - )->willReturn([ - $rect = (object) ['top' => 0, 'right' => 2, 'bottom' => 1, 'left' => 0], - $vp = (object) ['width' => 2, 'height' => 1], - ]); - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Unable to perform mouse out as the element takes up the entire viewport'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseOver(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_OVER); - $command->setTarget('id=cart'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseMove')->with($coord); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseUp(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_UP); - $command->setTarget('id=cart'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseUp')->with($coord); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testMouseUpAt(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::MOUSE_UP_AT); - $command->setTarget('id=cart'); - $command->setValue('5,10'); - $coord = $this->createMock(WebDriverCoordinates::class); - $element = $this->createMock(RemoteWebElement::class); - $element->expects($this->once())->method('getCoordinates')->willReturn($coord); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'cart' === $selector->getValue(); - }))->willReturn($element); - $mouse = $this->createMock(RemoteMouse::class); - $mouse->expects($this->once())->method('mouseMove')->with($coord, 5, 10)->willReturnSelf(); - $mouse->expects($this->once())->method('mouseUp'); - $this->driver->expects($this->once())->method('getMouse')->willReturn($mouse); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testSelect(): void - { - $command = new Command(); - $command->setCommand(MouseCommandRunner::SELECT); - $command->setTarget('partialLinkText=Language'); - $command->setValue('css=option[value=en_US]'); - $select = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'partial link text' === $selector->getMechanism() - && 'Language' === $selector->getValue(); - }))->willReturn($select); - $option = $this->createMock(WebDriverElement::class); - $option->expects($this->once())->method('click'); - $select->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && 'option[value=en_US]' === $selector->getValue(); - }))->willReturn($option); - $this->runner->run($command, $this->color, $this->driver); - } - - public function checkDataProvider(): array - { - return [ - [true, false], - [false, true], - ]; - } - - public function uncheckDataProvider(): array - { - return [ - [true, true], - [false, false], - ]; - } - - public function targetProvider(): array - { - return [ - [MouseCommandRunner::CLICK, null, false], - [MouseCommandRunner::CLICK, 'anything', false], - [MouseCommandRunner::CLICK, 'xpath=//path/to/element', true], - ]; - } -} diff --git a/tests/Command/Runner/RunnerTestCase.php b/tests/Command/Runner/RunnerTestCase.php deleted file mode 100644 index 40917c86..00000000 --- a/tests/Command/Runner/RunnerTestCase.php +++ /dev/null @@ -1,56 +0,0 @@ -driver = $this->createMock(RemoteWebDriver::class); - $this->runner = $this->createRunner(); - $this->color = $this->createMock(ColorInterface::class); - } - - abstract protected function createRunner(): CommandRunner; - - /** - * @dataProvider supportsCommandProvider - */ - public function testSupports(string $commandAsString, bool $supports): void - { - $command = new Command(); - $command->setCommand($commandAsString); - $this->assertSame($supports, $this->runner->supports($command)); - } - - public function supportsCommandProvider(): array - { - return [ - ...array_map(fn ($command) => [$command, true], $this->createRunner()->getAllCommands()), - ['invalidCommand', false], - ]; - } - - /** - * @dataProvider targetProvider - */ - public function testValidateTarget(string $commandString, $target, bool $valid): void - { - $command = new Command(); - $command->setCommand($commandString); - $command->setTarget($target); - $this->assertSame($valid, $this->runner->validateTarget($command)); - } - - abstract public function targetProvider(): array; -} diff --git a/tests/Command/Runner/ScriptCommandRunnerTest.php b/tests/Command/Runner/ScriptCommandRunnerTest.php deleted file mode 100644 index 7c9651b4..00000000 --- a/tests/Command/Runner/ScriptCommandRunnerTest.php +++ /dev/null @@ -1,71 +0,0 @@ -setCommand(ScriptCommandRunner::RUN_SCRIPT); - $command->setTarget('alert("Hello World!")'); - $this->color->expects($this->never())->method('getValues'); - $this->driver->expects($this->once())->method('executeScript')->with( - 'alert("Hello World!")', - [], - ); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testExecuteScript(): void - { - $command = new Command(); - $command->setCommand(ScriptCommandRunner::EXECUTE_SCRIPT); - $command->setTarget('return 2 + 1;'); - $command->setValue('total'); - $this->color->expects($this->never())->method('getValues'); - $this->color->expects($this->once())->method('setValue')->with('total', 3); - $this->driver->expects($this->once())->method('executeScript')->with( - 'return 2 + 1;', - [], - )->willReturn(3); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testExecuteAsyncScript(): void - { - $command = new Command(); - $command->setCommand(ScriptCommandRunner::EXECUTE_ASYNC_SCRIPT); - $command->setTarget('window.setTimeout(function() { return "Hello";}, 1000);'); - $command->setValue('message'); - $this->color->expects($this->never())->method('getValues'); - $this->color->expects($this->once())->method('setValue')->with('message', 'Hello'); - $this->driver->expects($this->once())->method('executeAsyncScript')->with( - 'window.setTimeout(function() { return "Hello";}, 1000);', - [], - )->willReturn('Hello'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function targetProvider(): array - { - return [ - [ScriptCommandRunner::RUN_SCRIPT, 'anything', true], - ]; - } -} diff --git a/tests/Command/Runner/StoreCommandRunnerTest.php b/tests/Command/Runner/StoreCommandRunnerTest.php deleted file mode 100644 index 018a48cc..00000000 --- a/tests/Command/Runner/StoreCommandRunnerTest.php +++ /dev/null @@ -1,148 +0,0 @@ -setCommand(StoreCommandRunner::STORE); - $command->setTarget('1'); - $command->setValue('count'); - $this->color->expects($this->once())->method('setValue')->with('count', '1'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testStoreAttribute(): void - { - $command = new Command(); - $command->setCommand(StoreCommandRunner::STORE_ATTRIBUTE); - $command->setTarget('css=.readmore@href'); - $command->setValue('readmoreLink'); - $this->color->expects($this->once())->method('setValue')->with('readmoreLink', 'http://example.com'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getAttribute')->with('href')->willReturn('http://example.com'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.readmore' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testStoreElementCount(): void - { - $command = new Command(); - $command->setCommand(StoreCommandRunner::STORE_ELEMENT_COUNT); - $command->setTarget('css=.item'); - $command->setValue('itemCount'); - $this->color->expects($this->once())->method('setValue')->with('itemCount', 2); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElements')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.item' === $selector->getValue(); - }))->willReturn([$element, $element]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testStoreJson(): void - { - $command = new Command(); - $command->setCommand(StoreCommandRunner::STORE_JSON); - $command->setTarget('{ "items": [1, 2, 3] }'); - $command->setValue('json'); - $this->color->expects($this->once())->method('setValue')->with( - 'json', - $this->callback(fn ($object) => $object instanceof \stdClass && $object->items === [1, 2, 3]) - ); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testStoreText(): void - { - $command = new Command(); - $command->setCommand(StoreCommandRunner::STORE_TEXT); - $command->setTarget('css=.head-line'); - $command->setValue('headLine'); - $this->color->expects($this->once())->method('setValue')->with('headLine', 'Welcome to our site'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getText')->willReturn('Welcome to our site'); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.head-line' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testStoreTitle(): void - { - $command = new Command(); - $command->setCommand(StoreCommandRunner::STORE_TITLE); - $command->setTarget('title'); - $this->color->expects($this->once())->method('setValue')->with('title', 'Welcome'); - $this->driver->expects($this->once())->method('getTitle')->willReturn('Welcome'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testStoreValue(): void - { - $command = new Command(); - $command->setCommand(StoreCommandRunner::STORE_VALUE); - $command->setTarget('css=.age'); - $command->setValue('age'); - $this->color->expects($this->once())->method('setValue')->with('age', 23); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('getAttribute')->with('value')->willReturn(23); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && '.age' === $selector->getValue(); - }))->willReturn($element); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testStoreWindowHandle(): void - { - $command = new Command(); - $command->setCommand(StoreCommandRunner::STORE_WINDOW_HANDLE); - $command->setTarget('windowHandle'); - $this->color->expects($this->once())->method('setValue')->with('windowHandle', 'window-123'); - $this->driver->expects($this->once())->method('getWindowHandle')->willReturn('window-123'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function targetProvider(): array - { - return [ - [StoreCommandRunner::STORE_ATTRIBUTE, null, false], - [StoreCommandRunner::STORE_ATTRIBUTE, 'anything', false], - [StoreCommandRunner::STORE_ATTRIBUTE, 'xpath=//path/to/element@attribute', true], - [StoreCommandRunner::STORE_TEXT, null, false], - [StoreCommandRunner::STORE_TEXT, 'anything', false], - [StoreCommandRunner::STORE_TEXT, 'xpath=//path/to/element', true], - [StoreCommandRunner::STORE_JSON, null, false], - [StoreCommandRunner::STORE_JSON, 'anything', false], - [StoreCommandRunner::STORE_JSON, '{"key": "value"}', true], - ]; - } -} diff --git a/tests/Command/Runner/WaitCommandRunnerTest.php b/tests/Command/Runner/WaitCommandRunnerTest.php deleted file mode 100644 index 9b146f23..00000000 --- a/tests/Command/Runner/WaitCommandRunnerTest.php +++ /dev/null @@ -1,188 +0,0 @@ -setCommand(WaitCommandRunner::WAIT_FOR_ELEMENT_EDITABLE); - $command->setTarget('id=name'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'name' === $selector->getValue(); - }))->willReturn($element); - $wait = $this->createMock(WebDriverWait::class); - $wait->expects($this->once())->method('until')->with($this->callback(function ($condition) { - return is_callable($condition) - && $condition instanceof Closure - && $condition(); - })); - $this->driver->expects($this->once())->method('wait')->willReturn($wait); - $this->driver - ->expects($this->once()) - ->method('executeScript') - ->with('return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', [$element]) - ->willReturn((object) ['enabled' => true, 'readonly' => false]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testWaitForElementNotEditable(): void - { - $command = new Command(); - $command->setCommand(WaitCommandRunner::WAIT_FOR_ELEMENT_NOT_EDITABLE); - $command->setTarget('id=avatar'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'avatar' === $selector->getValue(); - }))->willReturn($element); - $wait = $this->createMock(WebDriverWait::class); - $wait->expects($this->once())->method('until')->with($this->callback(function ($condition) { - return is_callable($condition) - && $condition instanceof Closure - && $condition(); - })); - $this->driver->expects($this->once())->method('wait')->willReturn($wait); - $this->driver - ->expects($this->once()) - ->method('executeScript') - ->with('return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', [$element]) - ->willReturn((object) ['enabled' => false, 'readonly' => true]); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testWaitForElementPresent(): void - { - $command = new Command(); - $command->setCommand(WaitCommandRunner::WAIT_FOR_ELEMENT_PRESENT); - $command->setTarget('id=title'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'title' === $selector->getValue(); - }))->willReturn($element); - $wait = $this->createMock(WebDriverWait::class); - $wait->expects($this->once())->method('until')->with($this->callback(function ($condition) { - return $condition instanceof WebDriverExpectedCondition - && call_user_func($condition->getApply(), $this->driver); - })); - $this->driver->expects($this->once())->method('wait')->willReturn($wait); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testNoElementsPresent(): void - { - $command = new Command(); - $command->setCommand(WaitCommandRunner::WAIT_FOR_ELEMENT_NOT_PRESENT); - $command->setTarget('css=button'); - $this->driver->expects($this->once())->method('findElements')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && 'button' === $selector->getValue(); - }))->willReturn([]); - $this->driver->expects($this->never())->method('wait'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testWaitForElementNotPresent(): void - { - $command = new Command(); - $command->setCommand(WaitCommandRunner::WAIT_FOR_ELEMENT_NOT_PRESENT); - $command->setTarget('css=button'); - $element = $this->createMock(WebDriverElement::class); - $element - ->expects($this->once()) - ->method('isEnabled') - ->willThrowException(new StaleElementReferenceException('Element gone')); - $this->driver->expects($this->once())->method('findElements')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'css selector' === $selector->getMechanism() - && 'button' === $selector->getValue(); - }))->willReturn([$element]); - $wait = $this->createMock(WebDriverWait::class); - $wait->expects($this->once())->method('until')->with($this->callback(function ($condition) { - return $condition instanceof WebDriverExpectedCondition - && call_user_func($condition->getApply(), $this->driver); - })); - $this->driver->expects($this->once())->method('wait')->willReturn($wait); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testWaitForElementVisible(): void - { - $command = new Command(); - $command->setCommand(WaitCommandRunner::WAIT_FOR_ELEMENT_VISIBLE); - $command->setTarget('id=title'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('isDisplayed')->willReturn(true); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'title' === $selector->getValue(); - }))->willReturn($element); - $wait = $this->createMock(WebDriverWait::class); - $wait->expects($this->once())->method('until')->with($this->callback(function ($condition) { - return $condition instanceof WebDriverExpectedCondition - && call_user_func($condition->getApply(), $this->driver); - })); - $this->driver->expects($this->once())->method('wait')->willReturn($wait); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testWaitForElementNotVisible(): void - { - $command = new Command(); - $command->setCommand(WaitCommandRunner::WAIT_FOR_ELEMENT_NOT_VISIBLE); - $command->setTarget('id=title'); - $element = $this->createMock(WebDriverElement::class); - $element->expects($this->once())->method('isDisplayed')->willReturn(false); - $this->driver->expects($this->once())->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'id' === $selector->getMechanism() - && 'title' === $selector->getValue(); - }))->willReturn($element); - $wait = $this->createMock(WebDriverWait::class); - $wait->expects($this->once())->method('until')->with($this->callback(function ($condition) { - return $condition instanceof WebDriverExpectedCondition - && call_user_func($condition->getApply(), $this->driver); - })); - $this->driver->expects($this->once())->method('wait')->willReturn($wait); - $this->runner->run($command, $this->color, $this->driver); - } - - public function targetProvider(): array - { - return [ - [WaitCommandRunner::WAIT_FOR_ELEMENT_VISIBLE, null, false], - [WaitCommandRunner::WAIT_FOR_ELEMENT_VISIBLE, 'anything', false], - [WaitCommandRunner::WAIT_FOR_ELEMENT_VISIBLE, 'xpath=//path/to/element', true], - ]; - } -} diff --git a/tests/Command/Runner/WindowCommandRunnerTest.php b/tests/Command/Runner/WindowCommandRunnerTest.php deleted file mode 100644 index 61ca8009..00000000 --- a/tests/Command/Runner/WindowCommandRunnerTest.php +++ /dev/null @@ -1,151 +0,0 @@ -setCommand(WindowCommandRunner::OPEN); - $command->setTarget('https://demo.sylius.com/en_US/'); - $this->driver->expects($this->once())->method('get')->with('https://demo.sylius.com/en_US/'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testSetWindowSize(): void - { - $command = new Command(); - $command->setCommand(WindowCommandRunner::SET_WINDOW_SIZE); - $command->setTarget('1280x800'); - $window = $this->createMock(WebDriverWindow::class); - $window->expects($this->once())->method('setSize')->with($this->callback(function ($dimention) { - return $dimention instanceof WebDriverDimension - && 1280 === $dimention->getWidth() - && 800 === $dimention->getHeight(); - })); - $options = $this->createMock(WebDriverOptions::class); - $options->expects($this->once())->method('window')->willReturn($window); - $this->driver->expects($this->once())->method('manage')->willReturn($options); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testSelectWindow(): void - { - $command = new Command(); - $command->setCommand(WindowCommandRunner::SELECT_WINDOW); - $command->setTarget('handle=testing'); - $targetLocator = $this->createMock(RemoteTargetLocator::class); - $targetLocator->expects($this->once())->method('window')->with('testing'); - $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testClose(): void - { - $command = new Command(); - $command->setCommand(WindowCommandRunner::CLOSE); - $this->driver->expects($this->once())->method('close'); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testSelectFrameRelativeTop(): void - { - $command = new Command(); - $command->setCommand(WindowCommandRunner::SELECT_FRAME); - $command->setTarget('relative=top'); - $targetLocator = $this->createMock(RemoteTargetLocator::class); - $targetLocator->expects($this->once())->method('defaultContent'); - $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testSelectFrameRelativeParent(): void - { - $command = new Command(); - $command->setCommand(WindowCommandRunner::SELECT_FRAME); - $command->setTarget('relative=parent'); - $targetLocator = $this->createMock(RemoteTargetLocator::class); - $targetLocator->expects($this->once())->method('parent'); - $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testSelectFrameIndex(): void - { - $command = new Command(); - $command->setCommand(WindowCommandRunner::SELECT_FRAME); - $command->setTarget('index=123'); - $targetLocator = $this->createMock(RemoteTargetLocator::class); - $targetLocator->expects($this->once())->method('frame')->with(123); - $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); - $this->runner->run($command, $this->color, $this->driver); - } - - public function testSelectFrameSelector(): void - { - $command = new Command(); - $command->setCommand(WindowCommandRunner::SELECT_FRAME); - $command->setTarget('linkText=Read More'); - $element = $this->createMock(WebDriverElement::class); - $this->driver->expects($this->exactly(2))->method('findElement')->with($this->callback(function ($selector) { - return $selector instanceof WebDriverBy - && 'link text' === $selector->getMechanism() - && 'Read More' === $selector->getValue(); - }))->willReturn($element); - $targetLocator = $this->createMock(RemoteTargetLocator::class); - $targetLocator->expects($this->once())->method('frame')->with($element); - $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); - $wait = $this->createMock(WebDriverWait::class); - $wait->expects($this->once())->method('until')->with($this->callback(function ($condition) { - return $condition instanceof WebDriverExpectedCondition - && call_user_func($condition->getApply(), $this->driver); - })); - $this->driver->expects($this->once())->method('wait')->willReturn($wait); - $this->runner->run($command, $this->color, $this->driver); - } - - public function targetProvider(): array - { - return [ - [WindowCommandRunner::OPEN, null, false], - [WindowCommandRunner::OPEN, 'anything', false], - [WindowCommandRunner::OPEN, 'http://example.com', true], - [WindowCommandRunner::SET_WINDOW_SIZE, null, false], - [WindowCommandRunner::SET_WINDOW_SIZE, 'anything', false], - [WindowCommandRunner::SET_WINDOW_SIZE, '123x234', true], - [WindowCommandRunner::SELECT_WINDOW, null, false], - [WindowCommandRunner::SELECT_WINDOW, 'anything', false], - [WindowCommandRunner::SELECT_WINDOW, 'handle=anything', true], - [WindowCommandRunner::SELECT_FRAME, null, false], - [WindowCommandRunner::SELECT_FRAME, 'anything', false], - [WindowCommandRunner::SELECT_FRAME, 'relative=top', true], - [WindowCommandRunner::SELECT_FRAME, 'relative=parent', true], - [WindowCommandRunner::SELECT_FRAME, 'index=123', true], - [WindowCommandRunner::SELECT_FRAME, 'xpath=//path/to/element', true], - ]; - } -} diff --git a/tests/Command/Script/ExecuteAsyncScriptCommandTest.php b/tests/Command/Script/ExecuteAsyncScriptCommandTest.php new file mode 100644 index 00000000..7cad1c49 --- /dev/null +++ b/tests/Command/Script/ExecuteAsyncScriptCommandTest.php @@ -0,0 +1,74 @@ +values->expects($this->never())->method('getValues'); + if ($value) { + $this->values->expects($this->once())->method('setValue')->with($value, 'Hello'); + } else { + $this->values->expects($this->never())->method('setValue'); + } + $this->driver->expects($this->once())->method('executeAsyncScript')->with( + 'window.setTimeout(function() { return "Hello";}, 1000);', + [], + )->willReturn('Hello'); + $this->command->run( + 'window.setTimeout(function() { return "Hello";}, 1000);', + $value, + $this->values, + $this->driver, + ); + } + + public function runProvider(): array + { + return [ + [null], + ['message'], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Script/ExecuteScriptCommandTest.php b/tests/Command/Script/ExecuteScriptCommandTest.php new file mode 100644 index 00000000..a7352e8c --- /dev/null +++ b/tests/Command/Script/ExecuteScriptCommandTest.php @@ -0,0 +1,69 @@ +values->expects($this->never())->method('getValues'); + if ($value) { + $this->values->expects($this->once())->method('setValue')->with($value, 3); + } else { + $this->values->expects($this->never())->method('setValue'); + } + $this->driver->expects($this->once())->method('executeScript')->with( + 'return 2 + 1;', + [], + )->willReturn(3); + $this->command->run('return 2 + 1;', $value, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [null], + ['total'], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Script/RunScriptCommandTest.php b/tests/Command/Script/RunScriptCommandTest.php new file mode 100644 index 00000000..85f561d2 --- /dev/null +++ b/tests/Command/Script/RunScriptCommandTest.php @@ -0,0 +1,53 @@ +values->expects($this->never())->method('getValues'); + $this->driver->expects($this->once())->method('executeScript')->with( + 'alert("Hello World!")', + [], + ); + $this->command->run('alert("Hello World!")', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Store/StoreAttributeCommandTest.php b/tests/Command/Store/StoreAttributeCommandTest.php new file mode 100644 index 00000000..1dd788fa --- /dev/null +++ b/tests/Command/Store/StoreAttributeCommandTest.php @@ -0,0 +1,70 @@ +values + ->expects($this->once()) + ->method('setValue') + ->with('readmoreLink', 'http://example.com'); + $this->element + ->expects($this->once()) + ->method('getAttribute') + ->with('href') + ->willReturn('http://example.com'); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.readmore' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('css=.readmore@href', 'readmoreLink', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['key@value', false], + ['css=#selector', false], + ['css=#selector@attr', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Store/StoreCommandTest.php b/tests/Command/Store/StoreCommandTest.php new file mode 100644 index 00000000..bfb80c9e --- /dev/null +++ b/tests/Command/Store/StoreCommandTest.php @@ -0,0 +1,49 @@ +values->expects($this->once())->method('setValue')->with('count', '1'); + $this->command->run('1', 'count', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Store/StoreElementCountCommandTest.php b/tests/Command/Store/StoreElementCountCommandTest.php new file mode 100644 index 00000000..2ff98d34 --- /dev/null +++ b/tests/Command/Store/StoreElementCountCommandTest.php @@ -0,0 +1,56 @@ +values->expects($this->once())->method('setValue')->with('itemCount', 2); + $this->driver->expects($this->once())->method('findElements')->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.item' === $selector->getValue(); + }))->willReturn([$this->element, $this->element]); + $this->command->run('css=.item', 'itemCount', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Store/StoreJsonCommandTest.php b/tests/Command/Store/StoreJsonCommandTest.php new file mode 100644 index 00000000..85d43792 --- /dev/null +++ b/tests/Command/Store/StoreJsonCommandTest.php @@ -0,0 +1,53 @@ +values->expects($this->once())->method('setValue')->with( + 'json', + $this->callback(fn ($object) => $object instanceof \stdClass && $object->items === [1, 2, 3]) + ); + $this->command->run('{ "items": [1, 2, 3] }', 'json', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['{"key": "value"}', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Store/StoreTextCommandTest.php b/tests/Command/Store/StoreTextCommandTest.php new file mode 100644 index 00000000..3e528114 --- /dev/null +++ b/tests/Command/Store/StoreTextCommandTest.php @@ -0,0 +1,64 @@ +values + ->expects($this->once()) + ->method('setValue') + ->with('headLine', 'Welcome to our site'); + $this->element->expects($this->once())->method('getText')->willReturn('Welcome to our site'); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.head-line' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('css=.head-line', 'headLine', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Store/StoreTitleCommandTest.php b/tests/Command/Store/StoreTitleCommandTest.php new file mode 100644 index 00000000..741a7d07 --- /dev/null +++ b/tests/Command/Store/StoreTitleCommandTest.php @@ -0,0 +1,50 @@ +values->expects($this->once())->method('setValue')->with('title', 'Welcome'); + $this->driver->expects($this->once())->method('getTitle')->willReturn('Welcome'); + $this->command->run('title', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Store/StoreValueCommandTest.php b/tests/Command/Store/StoreValueCommandTest.php new file mode 100644 index 00000000..de882984 --- /dev/null +++ b/tests/Command/Store/StoreValueCommandTest.php @@ -0,0 +1,61 @@ +values->expects($this->once())->method('setValue')->with('age', 23); + $this->element->expects($this->once())->method('getAttribute')->with('value')->willReturn(23); + $this->driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && '.age' === $selector->getValue(); + })) + ->willReturn($this->element); + $this->command->run('css=.age', 'age', $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Store/StoreWindowHandleCommandTest.php b/tests/Command/Store/StoreWindowHandleCommandTest.php new file mode 100644 index 00000000..d33acf63 --- /dev/null +++ b/tests/Command/Store/StoreWindowHandleCommandTest.php @@ -0,0 +1,50 @@ +values->expects($this->once())->method('setValue')->with('windowHandle', 'window-123'); + $this->driver->expects($this->once())->method('getWindowHandle')->willReturn('window-123'); + $this->command->run('windowHandle', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Wait/WaitForElementEditableCommandTest.php b/tests/Command/Wait/WaitForElementEditableCommandTest.php new file mode 100644 index 00000000..4ce01d45 --- /dev/null +++ b/tests/Command/Wait/WaitForElementEditableCommandTest.php @@ -0,0 +1,64 @@ +driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'name' === $selector->getValue(); + })) + ->willReturn($this->element); + $wait = $this->createMock(WebDriverWait::class); + $wait + ->expects($this->once()) + ->method('until') + ->with($this->callback(fn ($condition) => is_callable($condition) + && $condition instanceof Closure + && $editable === $condition()), 'Timed out waiting for element to be editable'); + $this->driver->expects($this->once())->method('wait')->with(123)->willReturn($wait); + $this->driver + ->expects($this->once()) + ->method('executeScript') + ->with( + 'return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', + [$this->element] + ) + ->willReturn((object) ['enabled' => $enabled, 'readonly' => $readonly]); + $this->command->run('id=name', 123, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [true, false, true], + [true, true, false], + [false, true, false], + [false, false, false], + ]; + } +} diff --git a/tests/Command/Wait/WaitForElementNotEditableCommandTest.php b/tests/Command/Wait/WaitForElementNotEditableCommandTest.php new file mode 100644 index 00000000..d4c70123 --- /dev/null +++ b/tests/Command/Wait/WaitForElementNotEditableCommandTest.php @@ -0,0 +1,64 @@ +driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'name' === $selector->getValue(); + })) + ->willReturn($this->element); + $wait = $this->createMock(WebDriverWait::class); + $wait + ->expects($this->once()) + ->method('until') + ->with($this->callback(fn ($condition) => is_callable($condition) + && $condition instanceof Closure + && !$editable === $condition()), 'Timed out waiting for element to not be editable'); + $this->driver->expects($this->once())->method('wait')->with(123)->willReturn($wait); + $this->driver + ->expects($this->once()) + ->method('executeScript') + ->with( + 'return { enabled: !arguments[0].disabled, readonly: arguments[0].readOnly };', + [$this->element] + ) + ->willReturn((object) ['enabled' => $enabled, 'readonly' => $readonly]); + $this->command->run('id=name', 123, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [true, false, true], + [true, true, false], + [false, true, false], + [false, false, false], + ]; + } +} diff --git a/tests/Command/Wait/WaitForElementNotPresentCommandTest.php b/tests/Command/Wait/WaitForElementNotPresentCommandTest.php new file mode 100644 index 00000000..8050fc26 --- /dev/null +++ b/tests/Command/Wait/WaitForElementNotPresentCommandTest.php @@ -0,0 +1,71 @@ +element); + $this->driver + ->expects($this->once()) + ->method('findElements') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'css selector' === $selector->getMechanism() + && 'button' === $selector->getValue(); + })) + ->willReturn($elements); + if ($elementsCount > 0) { + $mock = $elements[0] + ->expects($this->once()) + ->method('isEnabled'); + if ($stale) { + $mock->willThrowException(new StaleElementReferenceException('Element gone')); + } + $wait = $this->createMock(WebDriverWait::class); + $wait + ->expects($this->once()) + ->method('until') + ->with( + $this->callback(function (WebDriverExpectedCondition $condition) use ($stale) { + return $stale === call_user_func($condition->getApply(), $this->driver); + }) + ); + $this->driver->expects($this->once())->method('wait')->with(123)->willReturn($wait); + } else { + $this->driver->expects($this->never())->method('wait'); + } + $this->command->run('css=button', 123, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [0, false], + [1, false], + [2, false], + [1, true], + [2, true], + ]; + } +} diff --git a/tests/Command/Wait/WaitForElementNotVisibleCommandTest.php b/tests/Command/Wait/WaitForElementNotVisibleCommandTest.php new file mode 100644 index 00000000..adcedef6 --- /dev/null +++ b/tests/Command/Wait/WaitForElementNotVisibleCommandTest.php @@ -0,0 +1,70 @@ +driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'title' === $selector->getValue(); + })); + if ($present) { + $mock->willReturn($this->element); + $mock = $this->element + ->expects($this->once()) + ->method('isDisplayed'); + if ($stale) { + $mock->willThrowException(new StaleElementReferenceException('Element gone')); + } else { + $mock->willReturn($displayed); + } + } else { + $mock->willThrowException(new NoSuchElementException('Element missing')); + } + $wait = $this->createMock(WebDriverWait::class); + $wait + ->expects($this->once()) + ->method('until') + ->with($this->callback(function (WebDriverExpectedCondition $condition) use ($displayed, $present, $stale) { + return (!$displayed || !$present || $stale) === call_user_func($condition->getApply(), $this->driver); + })); + $this->driver->expects($this->once())->method('wait')->with(123)->willReturn($wait); + $this->command->run('id=title', 123, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [false, true, false], + [true, true, false], + [false, false, false], + [false, true, true], + ]; + } +} diff --git a/tests/Command/Wait/WaitForElementPresentCommandTest.php b/tests/Command/Wait/WaitForElementPresentCommandTest.php new file mode 100644 index 00000000..89d7ea62 --- /dev/null +++ b/tests/Command/Wait/WaitForElementPresentCommandTest.php @@ -0,0 +1,59 @@ +driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'title' === $selector->getValue(); + })); + if ($present) { + $mock->willReturn($this->element); + } else { + $mock->willThrowException(new NoSuchElementException('Element missing')); + } + $wait = $this->createMock(WebDriverWait::class); + $wait + ->expects($this->once()) + ->method('until') + ->with($this->callback(function (WebDriverExpectedCondition $condition) use ($present) { + return $present === !empty(call_user_func($condition->getApply(), $this->driver)); + })); + $this->driver->expects($this->once())->method('wait')->with(123)->willReturn($wait); + $this->command->run('id=title', 123, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [false], + [true], + ]; + } +} diff --git a/tests/Command/Wait/WaitForElementVisibleCommandTest.php b/tests/Command/Wait/WaitForElementVisibleCommandTest.php new file mode 100644 index 00000000..90dde3e5 --- /dev/null +++ b/tests/Command/Wait/WaitForElementVisibleCommandTest.php @@ -0,0 +1,64 @@ +driver + ->expects($this->once()) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'id' === $selector->getMechanism() + && 'title' === $selector->getValue(); + })) + ->willReturn($this->element); + $mock = $this->element + ->expects($this->once()) + ->method('isDisplayed'); + if ($stale) { + $mock->willThrowException(new StaleElementReferenceException('Element gone')); + } else { + $mock->willReturn($displayed); + } + $wait = $this->createMock(WebDriverWait::class); + $wait + ->expects($this->once()) + ->method('until') + ->with($this->callback(function (WebDriverExpectedCondition $condition) use ($displayed, $stale) { + return ($displayed && !$stale) === !empty(call_user_func($condition->getApply(), $this->driver)); + })); + $this->driver->expects($this->once())->method('wait')->with(123)->willReturn($wait); + $this->command->run('id=title', 123, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + [true, false], + [false, false], + [false, true], + ]; + } +} diff --git a/tests/Command/Wait/WaitTestCase.php b/tests/Command/Wait/WaitTestCase.php new file mode 100644 index 00000000..258730ee --- /dev/null +++ b/tests/Command/Wait/WaitTestCase.php @@ -0,0 +1,34 @@ +driver->expects($this->once())->method('close'); + $this->command->run(null, null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Window/OpenCommandTest.php b/tests/Command/Window/OpenCommandTest.php new file mode 100644 index 00000000..23eff101 --- /dev/null +++ b/tests/Command/Window/OpenCommandTest.php @@ -0,0 +1,50 @@ +driver->expects($this->once())->method('get')->with('https://demo.sylius.com/en_US/'); + $this->command->run('https://demo.sylius.com/en_US/', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['http://domain.example/path', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Window/SelectFrameCommandTest.php b/tests/Command/Window/SelectFrameCommandTest.php new file mode 100644 index 00000000..4f3a9fe5 --- /dev/null +++ b/tests/Command/Window/SelectFrameCommandTest.php @@ -0,0 +1,93 @@ +driver + ->expects($this->exactly(2)) + ->method('findElement') + ->with($this->callback(function ($selector) { + return $selector instanceof WebDriverBy + && 'link text' === $selector->getMechanism() + && 'Read More' === $selector->getValue(); + })) + ->willReturn($this->element); + $params = [$this->element]; + $wait = $this->createMock(WebDriverWait::class); + $wait + ->expects($this->once()) + ->method('until') + ->with($this->callback(function ($condition) { + return $condition instanceof WebDriverExpectedCondition + && call_user_func($condition->getApply(), $this->driver); + })); + $this->driver->expects($this->once())->method('wait')->willReturn($wait); + } + $targetLocator = $this->createMock(RemoteTargetLocator::class); + $targetLocator->expects($this->once())->method($method)->with(...$params); + $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); + $this->command->run($target, null, $this->values, $this->driver); + } + + public function runProvider(): array + { + return [ + ['relative=top', 'defaultContent', []], + ['relative=parent', 'parent', []], + ['index=123', 'frame', [123]], + ['linkText=Read More', 'frame', null], + ]; + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['relative=top', true], + ['relative=parent', true], + ['index=123', true], + ['css=#selector', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Window/SelectWindowCommandTest.php b/tests/Command/Window/SelectWindowCommandTest.php new file mode 100644 index 00000000..d8537cb5 --- /dev/null +++ b/tests/Command/Window/SelectWindowCommandTest.php @@ -0,0 +1,54 @@ +createMock(RemoteTargetLocator::class); + $targetLocator->expects($this->once())->method('window')->with('testing'); + $this->driver->expects($this->once())->method('switchTo')->willReturn($targetLocator); + $this->command->run('handle=testing', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['handle=', false], + ['handle=ABC-123', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/Command/Window/SetWindowSizeCommandTest.php b/tests/Command/Window/SetWindowSizeCommandTest.php new file mode 100644 index 00000000..7e109a02 --- /dev/null +++ b/tests/Command/Window/SetWindowSizeCommandTest.php @@ -0,0 +1,65 @@ +createMock(WebDriverWindow::class); + $window->expects($this->once())->method('setSize')->with($this->callback(function ($dimention) { + return $dimention instanceof WebDriverDimension + && 1280 === $dimention->getWidth() + && 800 === $dimention->getHeight(); + })); + $options = $this->createMock(WebDriverOptions::class); + $options->expects($this->once())->method('window')->willReturn($window); + $this->driver->expects($this->once())->method('manage')->willReturn($options); + $this->command->run('1280x800', null, $this->values, $this->driver); + } + + public function targetProvider(): array + { + return [ + [null, false], + ['', false], + ['anything', false], + ['1+2', false], + ['720p', false], + ['1280x', false], + ['x720', false], + ['1280x720', true], + ]; + } + + public function valueProvider(): array + { + return [ + [null, true], + ['', true], + ['anything', true], + ]; + } +} diff --git a/tests/DependencyInjection/Compiler/PluginPassTest.php b/tests/DependencyInjection/Compiler/PluginPassTest.php index 06e6f7de..dd6e59f9 100644 --- a/tests/DependencyInjection/Compiler/PluginPassTest.php +++ b/tests/DependencyInjection/Compiler/PluginPassTest.php @@ -16,7 +16,7 @@ /** * @covers \Tienvx\Bundle\MbtBundle\DependencyInjection\Compiler\PluginPass - * @covers \Tienvx\Bundle\MbtBundle\Plugin\AbstractPluginManager + * @covers \Tienvx\Bundle\MbtBundle\Plugin\PluginManager */ class PluginPassTest extends TestCase { diff --git a/tests/DependencyInjection/TienvxMbtExtensionTest.php b/tests/DependencyInjection/TienvxMbtExtensionTest.php deleted file mode 100644 index 8403c8ca..00000000 --- a/tests/DependencyInjection/TienvxMbtExtensionTest.php +++ /dev/null @@ -1,58 +0,0 @@ - 'http://localhost:4444', - ]]; - - protected ContainerBuilder $container; - protected TienvxMbtExtension $loader; - - protected function setUp(): void - { - $this->container = new ContainerBuilder(); - $this->loader = new TienvxMbtExtension(); - } - - public function testExceptionMissingSeleniumServer(): void - { - $this->expectException(InvalidConfigurationException::class); - $loader = new TienvxMbtExtension(); - $loader->load([], new ContainerBuilder()); - } - - public function testUpdateServiceDefinitions(): void - { - $this->loader->load(static::CONFIGS, $this->container); - $this->assertSame([ - ['setWebdriverUri', [static::CONFIGS[0][Configuration::WEBDRIVER_URI]]], - ], $this->container->findDefinition(SelenoidHelperInterface::class)->getMethodCalls()); - } - - public function testRegisterForAutoconfiguration(): void - { - $this->loader->load(static::CONFIGS, $this->container); - $interfaces = [PluginInterface::class, CommandRunnerInterface::class]; - foreach ($interfaces as $interface) { - $autoConfigured = $this->container->getAutoconfiguredInstanceof()[$interface]; - $this->assertTrue($autoConfigured->hasTag($interface::TAG)); - $this->assertTrue($autoConfigured->isLazy()); - } - } -} diff --git a/tests/Entity/Bug/VideoTest.php b/tests/Entity/Bug/VideoTest.php new file mode 100644 index 00000000..5847beb8 --- /dev/null +++ b/tests/Entity/Bug/VideoTest.php @@ -0,0 +1,20 @@ +prePersist(); - $this->assertInstanceOf(\DateTime::class, $bug->getCreatedAt()); - $this->assertInstanceOf(\DateTime::class, $bug->getUpdatedAt()); + $this->bug->prePersist(); + $this->assertInstanceOf(\DateTime::class, $this->bug->getCreatedAt()); + $this->assertInstanceOf(\DateTime::class, $this->bug->getUpdatedAt()); } public function testPreUpdate(): void { - $bug = new Bug(); - $bug->prePersist(); - $bug->preUpdate(); - $this->assertInstanceOf(\DateTime::class, $bug->getUpdatedAt()); + $this->bug->prePersist(); + $this->bug->preUpdate(); + $this->assertInstanceOf(\DateTime::class, $updatedAt = $this->bug->getUpdatedAt()); + $this->bug->preUpdate(); + $this->assertTrue( + $this->bug->getUpdatedAt() instanceof \DateTime + && $updatedAt !== $this->bug->getUpdatedAt() + ); } public function testValidateInvalidBug(): void { - $bug = new Bug(); - $bug->setTitle(''); + $this->bug->setTitle(''); $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator(); - $violations = $validator->validate($bug); - $this->assertCount(1, $violations); + $violations = $validator->validate($this->bug); + $this->assertCount(3, $violations); $message = 'Object(Tienvx\Bundle\MbtBundle\Entity\Bug).title: This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) +Object(Tienvx\Bundle\MbtBundle\Entity\Bug).steps[0].places: + mbt.bug.missing_places_in_step (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) +Object(Tienvx\Bundle\MbtBundle\Entity\Bug).steps[1].places: + mbt.bug.missing_places_in_step (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) '; $this->assertSame($message, (string) $violations); } + + protected function createBug(): BugInterface + { + return new Bug(); + } } diff --git a/tests/Entity/Model/RevisionTest.php b/tests/Entity/Model/RevisionTest.php index ceffe6d3..6a7f116d 100644 --- a/tests/Entity/Model/RevisionTest.php +++ b/tests/Entity/Model/RevisionTest.php @@ -2,46 +2,120 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Entity\Model; -use PHPUnit\Framework\TestCase; -use Tienvx\Bundle\MbtBundle\Entity\Model; +use Symfony\Component\Validator\Validation; +use Symfony\Component\Validator\Validator\ValidatorInterface; use Tienvx\Bundle\MbtBundle\Entity\Model\Revision; use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; -use Tienvx\Bundle\MbtBundle\Model\ModelInterface; +use Tienvx\Bundle\MbtBundle\Tests\Fixtures\Validator\CustomConstraintValidatorFactory; +use Tienvx\Bundle\MbtBundle\Tests\Model\Model\RevisionTest as RevisionModelTest; /** + * @covers \Tienvx\Bundle\MbtBundle\Entity\Model\Revision + * + * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision * @uses \Tienvx\Bundle\MbtBundle\Entity\Model * @uses \Tienvx\Bundle\MbtBundle\Model\Model - * @covers \Tienvx\Bundle\MbtBundle\Entity\Model\Revision - * @covers \Tienvx\Bundle\MbtBundle\Model\Model\Revision + * @uses \Tienvx\Bundle\MbtBundle\Command\CommandManager + * @uses \Tienvx\Bundle\MbtBundle\Command\AbstractCommand + * @uses \Tienvx\Bundle\MbtBundle\Command\Mouse\ClickAtCommand + * @uses \Tienvx\Bundle\MbtBundle\Command\Mouse\ClickCommand + * @uses \Tienvx\Bundle\MbtBundle\Validator\ValidCommandValidator + * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command + * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision\Place + * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision\Transition + * @uses \Tienvx\Bundle\MbtBundle\Validator\TagsValidator + * @uses \Tienvx\Bundle\MbtBundle\Validator\ValidCommand + * @uses \Tienvx\Bundle\MbtBundle\ValueObject\Model\Command */ -class RevisionTest extends TestCase +class RevisionTest extends RevisionModelTest { - protected RevisionInterface $revision; - protected RevisionInterface $activeRevision; - protected ModelInterface $model; + protected ValidatorInterface $validator; protected function setUp(): void { - $this->revision = new Revision(); - $this->revision->setId(1); - $this->activeRevision = new Revision(); - $this->activeRevision->setId(2); - $this->model = new Model(); - $this->model->setLabel('Model label'); + parent::setUp(); + $this->validator = Validation::createValidatorBuilder() + ->enableAnnotationMapping() + ->setConstraintValidatorFactory(new CustomConstraintValidatorFactory()) + ->getValidator(); + } + + public function testValidateInvalidRevision(): void + { + $violations = $this->validator->validate($this->revision); + $this->assertCount(11, $violations); + $message = 'Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions[0].toPlaces: + mbt.model.places_invalid +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions[1].fromPlaces: + mbt.model.places_invalid +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions: + mbt.model.missing_start_transition +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[0].label: + This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[0].commands[0].command: + mbt.model.command.invalid_command (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[0].commands[0].command: + This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[0].commands[1].target: + mbt.model.command.required_target (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[1].commands[0].command: + mbt.model.command.invalid_command (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[1].commands[1].value: + mbt.model.command.required_value (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions[1].label: + This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions[1].toPlaces: + mbt.model.missing_to_places (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) +'; + $this->assertSame($message, (string) $violations); + } + + public function testValidateInvalidRevisionTooManyStartTransitions(): void + { + $this->revision->getTransition(0)->setFromPlaces([]); + $this->revision->getTransition(1)->setFromPlaces([]); + $violations = $this->validator->validate($this->revision); + $this->assertCount(11, $violations); + $message = 'Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions[0].toPlaces: + mbt.model.places_invalid +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions[1]: + mbt.model.missing_places +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions: + mbt.model.too_many_start_transitions +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[0].label: + This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[0].commands[0].command: + mbt.model.command.invalid_command (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[0].commands[0].command: + This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[0].commands[1].target: + mbt.model.command.required_target (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[1].commands[0].command: + mbt.model.command.invalid_command (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places[1].commands[1].value: + mbt.model.command.required_value (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions[1].label: + This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions[1].toPlaces: + mbt.model.missing_to_places (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) +'; + $this->assertSame($message, (string) $violations); } - public function testConvertToString(): void + public function testNoPlacesAndTransitions(): void { - $this->assertSame('', (string) $this->revision); - $this->model->setActiveRevision($this->revision); - $this->assertSame($this->model->getLabel(), (string) $this->revision); + $violations = $this->validator->validate($this->createRevision()); + $this->assertCount(2, $violations); + $message = 'Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).places: + This collection should contain 1 element or more. (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) +Object(Tienvx\Bundle\MbtBundle\Entity\Model\Revision).transitions: + This collection should contain 1 element or more. (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) +'; + $this->assertSame($message, (string) $violations); } - public function testIsLatest(): void + protected function createRevision(): RevisionInterface { - $this->model->setActiveRevision($this->revision); - $this->model->setActiveRevision($this->activeRevision); - $this->assertFalse($this->revision->isLatest()); - $this->assertTrue($this->activeRevision->isLatest()); + return new Revision(); } } diff --git a/tests/Entity/ModelTest.php b/tests/Entity/ModelTest.php index 2da76159..8a1afb1e 100644 --- a/tests/Entity/ModelTest.php +++ b/tests/Entity/ModelTest.php @@ -2,279 +2,40 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Entity; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Validator\Validation; -use Symfony\Component\Validator\Validator\ValidatorInterface; use Tienvx\Bundle\MbtBundle\Entity\Model; -use Tienvx\Bundle\MbtBundle\Entity\Model\Revision; -use Tienvx\Bundle\MbtBundle\Tests\Fixtures\Validator\CustomConstraintValidatorFactory; -use Tienvx\Bundle\MbtBundle\ValueObject\Model\Command; -use Tienvx\Bundle\MbtBundle\ValueObject\Model\Place; -use Tienvx\Bundle\MbtBundle\ValueObject\Model\Transition; +use Tienvx\Bundle\MbtBundle\Model\ModelInterface; +use Tienvx\Bundle\MbtBundle\Tests\Model\ModelTest as ModelModelTest; /** * @covers \Tienvx\Bundle\MbtBundle\Entity\Model * @covers \Tienvx\Bundle\MbtBundle\Model\Model * - * @uses \Tienvx\Bundle\MbtBundle\Command\CommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\CommandRunnerManager - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\AlertCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\AssertionRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\KeyboardCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\MouseCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\ScriptCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\StoreCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\WaitCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\WindowCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Validator\ValidCommandValidator * @uses \Tienvx\Bundle\MbtBundle\Entity\Model\Revision * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision - * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command - * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision\Place - * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision\Transition - * @uses \Tienvx\Bundle\MbtBundle\Validator\TagsValidator - * @uses \Tienvx\Bundle\MbtBundle\Validator\ValidCommand - * @uses \Tienvx\Bundle\MbtBundle\ValueObject\Model\Command */ -class ModelTest extends TestCase +class ModelTest extends ModelModelTest { - protected Model $model; - protected Revision $revision; - protected ValidatorInterface $validator; - - protected function setUp(): void - { - $this->revision = new Revision(); - $this->revision->setId(1); - $places = [ - $p1 = new Place(), - $p2 = new Place(), - ]; - $p1->setLabel(''); - $p1->setCommands([ - $c1 = new Command(), - $c2 = new Command(), - ]); - $c1->setCommand(''); - $c1->setTarget('css=.name'); - $c1->setValue('test'); - $c2->setCommand('click'); - $c2->setTarget(null); - $c2->setValue('test'); - $p2->setLabel('p2'); - $p2->setCommands([ - $c3 = new Command(), - $c4 = new Command(), - ]); - $c3->setCommand('doNoThing'); - $c3->setTarget('css=.about'); - $c3->setValue('test'); - $c4->setCommand('clickAt'); - $c4->setTarget('css=.avatar'); - $c4->setValue(null); - $this->revision->setPlaces($places); - $transitions = [ - $t1 = new Transition(), - $t2 = new Transition(), - ]; - $t1->setLabel('t1'); - $t1->setFromPlaces([1]); - $t1->setToPlaces([1, 2]); - $t2->setLabel(''); - $t2->setFromPlaces([1, 2]); - $t2->setToPlaces([]); - $t2->setGuard('count > 1'); - $this->revision->setTransitions($transitions); - - $this->model = new Model(); - $this->model->setLabel(''); - $this->model->setTags('tag1,tag1,tag2,,tag3'); - $this->model->setActiveRevision($this->revision); - - $this->validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping() - ->setConstraintValidatorFactory(new CustomConstraintValidatorFactory()) - ->getValidator(); - } - public function testPrePersist(): void { - $model = new Model(); - $model->prePersist(); - $this->assertInstanceOf(\DateTime::class, $model->getCreatedAt()); - $this->assertInstanceOf(\DateTime::class, $model->getUpdatedAt()); + $this->model->prePersist(); + $this->assertInstanceOf(\DateTime::class, $this->model->getCreatedAt()); + $this->assertInstanceOf(\DateTime::class, $this->model->getUpdatedAt()); } public function testPreUpdate(): void { - $model = new Model(); - $model->prePersist(); - $model->preUpdate(); - $this->assertInstanceOf(\DateTime::class, $updatedAt = $model->getUpdatedAt()); - $model->preUpdate(); - $this->assertTrue($model->getUpdatedAt() instanceof \DateTime && $updatedAt !== $model->getUpdatedAt()); - } - - public function testActiveRevision(): void - { - $model = new Model(); - $model->setId(1); - $model->setActiveRevision($this->revision); - $this->assertSame($this->revision->getId(), $model->getActiveRevision()->getId()); - $this->assertSame($model, $this->revision->getModel()); - $this->assertSame($model->getId(), $this->revision->getModel()->getId()); - $revision = new Revision(); - $revision->setId(2); - $model->setActiveRevision($revision); - $this->assertNotSame($this->revision->getId(), $model->getActiveRevision()->getId()); - $this->assertSame($model, $revision->getModel()); - $this->assertSame($model->getId(), $revision->getModel()->getId()); - } - - public function testAddRemoveRevision(): void - { - $this->assertTrue($this->model->getRevisions()->contains($this->revision)); - $this->model->removeRevision($this->revision); - $this->assertFalse($this->model->getRevisions()->contains($this->revision)); - $this->assertCount(0, $this->model->getRevisions()); - $revision = new Revision(); - $this->model->addRevision($revision); - $this->assertTrue($this->model->getRevisions()->contains($revision)); - $this->assertCount(1, $this->model->getRevisions()); - } - - public function testValidateInvalidModel(): void - { - $violations = $this->validator->validate($this->model); - $this->assertCount(13, $violations); - $message = 'Object(Tienvx\Bundle\MbtBundle\Entity\Model).label: - This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).tags: - The tags should be unique and not blank. (code 628fca96-35f8-11eb-adc1-0242ac120002) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions[0].toPlaces: - mbt.model.places_invalid -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions[1].fromPlaces: - mbt.model.places_invalid -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions: - mbt.model.missing_start_transition -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[0].label: - This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[0].commands[0].command: - mbt.model.command.invalid_command (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[0].commands[0].command: - This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[0].commands[1].target: - mbt.model.command.required_target (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[1].commands[0].command: - mbt.model.command.invalid_command (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[1].commands[1].value: - mbt.model.command.required_value (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions[1].label: - This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions[1].toPlaces: - mbt.model.missing_to_places (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) -'; - $this->assertSame($message, (string) $violations); - } - - public function testValidateInvalidModelTooManyStartTransitions(): void - { - $this->model->getActiveRevision()->getTransition(0)->setFromPlaces([]); - $this->model->getActiveRevision()->getTransition(1)->setFromPlaces([]); - $violations = $this->validator->validate($this->model); - $this->assertCount(12, $violations); - $message = 'Object(Tienvx\Bundle\MbtBundle\Entity\Model).label: - This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).tags: - The tags should be unique and not blank. (code 628fca96-35f8-11eb-adc1-0242ac120002) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions[0].toPlaces: - mbt.model.places_invalid -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions: - mbt.model.too_many_start_transitions -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[0].label: - This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[0].commands[0].command: - mbt.model.command.invalid_command (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[0].commands[0].command: - This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[0].commands[1].target: - mbt.model.command.required_target (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[1].commands[0].command: - mbt.model.command.invalid_command (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.places[1].commands[1].value: - mbt.model.command.required_value (code ba5fd751-cbdf-45ab-a1e7-37045d5ef44b) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions[1].label: - This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) -Object(Tienvx\Bundle\MbtBundle\Entity\Model).activeRevision.transitions[1].toPlaces: - mbt.model.missing_to_places (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) -'; - $this->assertSame($message, (string) $violations); + $this->model->prePersist(); + $this->model->preUpdate(); + $this->assertInstanceOf(\DateTime::class, $updatedAt = $this->model->getUpdatedAt()); + $this->model->preUpdate(); + $this->assertTrue( + $this->model->getUpdatedAt() instanceof \DateTime + && $updatedAt !== $this->model->getUpdatedAt() + ); } - public function testToArray(): void + protected function createModel(): ModelInterface { - $this->assertSame([ - 'label' => '', - 'tags' => 'tag1,tag1,tag2,,tag3', - 'places' => [ - 0 => [ - 'label' => '', - 'commands' => [ - 0 => [ - 'command' => '', - 'target' => 'css=.name', - 'value' => 'test', - ], - 1 => [ - 'command' => 'click', - 'target' => null, - 'value' => 'test', - ], - ], - ], - 1 => [ - 'label' => 'p2', - 'commands' => [ - 0 => [ - 'command' => 'doNoThing', - 'target' => 'css=.about', - 'value' => 'test', - ], - 1 => [ - 'command' => 'clickAt', - 'target' => 'css=.avatar', - 'value' => null, - ], - ], - ], - ], - 'transitions' => [ - 0 => [ - 'label' => 't1', - 'guard' => null, - 'fromPlaces' => [ - 0 => 1, - ], - 'toPlaces' => [ - 0 => 1, - 1 => 2, - ], - 'commands' => [ - ], - ], - 1 => [ - 'label' => '', - 'guard' => 'count > 1', - 'fromPlaces' => [ - 0 => 1, - 1 => 2, - ], - 'toPlaces' => [ - ], - 'commands' => [ - ], - ], - ], - ], $this->model->toArray()); + return new Model(); } } diff --git a/tests/Entity/ProgressTest.php b/tests/Entity/ProgressTest.php index 5c5c54fe..33952b94 100644 --- a/tests/Entity/ProgressTest.php +++ b/tests/Entity/ProgressTest.php @@ -2,28 +2,33 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Entity; -use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Validation; use Tienvx\Bundle\MbtBundle\Entity\Progress; +use Tienvx\Bundle\MbtBundle\Model\ProgressInterface; +use Tienvx\Bundle\MbtBundle\Tests\Model\ProgressTest as ProgressModelTest; /** * @covers \Tienvx\Bundle\MbtBundle\Entity\Progress * @covers \Tienvx\Bundle\MbtBundle\Model\Progress */ -class ProgressTest extends TestCase +class ProgressTest extends ProgressModelTest { public function testValidateInvalidProgress(): void { - $progress = new Progress(); - $progress->setTotal(10); - $progress->setProcessed(11); + $this->progress->setTotal(10); + $this->progress->setProcessed(11); $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator(); - $violations = $validator->validate($progress); + $violations = $validator->validate($this->progress); $this->assertCount(1, $violations); $message = 'Object(Tienvx\Bundle\MbtBundle\Entity\Progress).processed: Processed should be less than or equal to total. '; $this->assertSame($message, (string) $violations); } + + protected function createProgress(): ProgressInterface + { + return new Progress(); + } } diff --git a/tests/Entity/Task/BrowserTest.php b/tests/Entity/Task/BrowserTest.php new file mode 100644 index 00000000..aa3fbe35 --- /dev/null +++ b/tests/Entity/Task/BrowserTest.php @@ -0,0 +1,20 @@ +prePersist(); - $this->assertInstanceOf(\DateTime::class, $task->getCreatedAt()); - $this->assertInstanceOf(\DateTime::class, $task->getUpdatedAt()); + $this->task->prePersist(); + $this->assertInstanceOf(\DateTime::class, $this->task->getCreatedAt()); + $this->assertInstanceOf(\DateTime::class, $this->task->getUpdatedAt()); } public function testPreUpdate(): void { - $task = new Task(); - $task->prePersist(); - $task->preUpdate(); - $this->assertInstanceOf(\DateTime::class, $task->getUpdatedAt()); + $this->task->prePersist(); + $this->task->preUpdate(); + $this->assertInstanceOf(\DateTime::class, $updatedAt = $this->task->getUpdatedAt()); + $this->task->preUpdate(); + $this->assertTrue( + $this->task->getUpdatedAt() instanceof \DateTime + && $updatedAt !== $this->task->getUpdatedAt() + ); + } + + public function testValidateInvalidBug(): void + { + $this->task->setTitle(''); + + $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator(); + $violations = $validator->validate($this->task); + $this->assertCount(3, $violations); + $message = 'Object(Tienvx\Bundle\MbtBundle\Entity\Task).title: + This value should not be blank. (code c1051bb4-d103-4f74-8988-acbcafc7fdc3) +Object(Tienvx\Bundle\MbtBundle\Entity\Task).modelRevision.places: + This collection should contain 1 element or more. (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) +Object(Tienvx\Bundle\MbtBundle\Entity\Task).modelRevision.transitions: + This collection should contain 1 element or more. (code bef8e338-6ae5-4caf-b8e2-50e7b0579e69) +'; + $this->assertSame($message, (string) $violations); + } + + protected function createTask(): TaskInterface + { + return new Task(); } } diff --git a/tests/EventListener/EntitySubscriberTest.php b/tests/EventListener/EntitySubscriberTest.php index 673fbabb..5e4031d8 100644 --- a/tests/EventListener/EntitySubscriberTest.php +++ b/tests/EventListener/EntitySubscriberTest.php @@ -5,6 +5,7 @@ use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Events; use Doctrine\Persistence\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; @@ -21,9 +22,16 @@ */ class EntitySubscriberTest extends TestCase { + protected MessageBusInterface|MockObject $messageBus; + + protected function setUp(): void + { + $this->messageBus = $this->createMock(MessageBusInterface::class); + } + public function testGetSubscribedEvents(): void { - $subscriber = new EntitySubscriber($this->createMock(MessageBusInterface::class)); + $subscriber = new EntitySubscriber($this->messageBus); $this->assertSame([ Events::postPersist, ], $subscriber->getSubscribedEvents()); @@ -33,13 +41,12 @@ public function testPostPersistBug(): void { $bug = new Bug(); $bug->setId(23); - $messageBus = $this->createMock(MessageBusInterface::class); - $messageBus + $this->messageBus ->expects($this->once()) ->method('dispatch') ->with($this->callback(fn ($message) => $message instanceof ReduceBugMessage && 23 === $message->getId())) ->willReturn(new Envelope(new \stdClass())); - $subscriber = new EntitySubscriber($messageBus); + $subscriber = new EntitySubscriber($this->messageBus); $args = new LifecycleEventArgs($bug, $this->createMock(ObjectManager::class)); $subscriber->postPersist($args); } diff --git a/tests/Factory/Model/Revision/CommandFactoryTest.php b/tests/Factory/Model/Revision/CommandFactoryTest.php index 087a8c89..58e256fb 100644 --- a/tests/Factory/Model/Revision/CommandFactoryTest.php +++ b/tests/Factory/Model/Revision/CommandFactoryTest.php @@ -3,7 +3,6 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Factory\Model\Revision; use PHPUnit\Framework\TestCase; -use Tienvx\Bundle\MbtBundle\Command\Runner\StoreCommandRunner; use Tienvx\Bundle\MbtBundle\Factory\Model\Revision\CommandFactory; /** @@ -18,7 +17,7 @@ class CommandFactoryTest extends TestCase protected function setUp(): void { $this->data = [ - 'command' => StoreCommandRunner::STORE, + 'command' => 'store', 'target' => '123', 'value' => 'var', ]; diff --git a/tests/Factory/Model/Revision/TransitionFactoryTest.php b/tests/Factory/Model/Revision/TransitionFactoryTest.php index 67a72752..24f71d5b 100644 --- a/tests/Factory/Model/Revision/TransitionFactoryTest.php +++ b/tests/Factory/Model/Revision/TransitionFactoryTest.php @@ -20,6 +20,7 @@ protected function setUp(): void $this->data = [ 'label' => 'Transition 1', 'guard' => 'count > 1', + 'expression' => 'count = count + 1', 'fromPlaces' => [1, 2], 'toPlaces' => [2, 3], 'commands' => [], @@ -31,6 +32,7 @@ public function testCreateFromArray(): void $transition = TransitionFactory::createFromArray($this->data); $this->assertSame('Transition 1', $transition->getLabel()); $this->assertSame('count > 1', $transition->getGuard()); + $this->assertSame('count = count + 1', $transition->getExpression()); $this->assertSame([1, 2], $transition->getFromPlaces()); $this->assertSame([2, 3], $transition->getToPlaces()); $this->assertIsArray($transition->getCommands()); diff --git a/tests/Fixtures/Exception/HttpClientException.php b/tests/Fixtures/Exception/HttpClientException.php new file mode 100644 index 00000000..3894ed7a --- /dev/null +++ b/tests/Fixtures/Exception/HttpClientException.php @@ -0,0 +1,9 @@ +validatedBy(); if (ValidCommandValidator::class === $className && !isset($this->validators[$className])) { - $this->validators[$className] = new ValidCommandValidator(new CommandRunnerManager( - [ - new AlertCommandRunner(), - new AssertionRunner(), - new KeyboardCommandRunner(), - new MouseCommandRunner(), - new ScriptCommandRunner(), - new StoreCommandRunner(), - new WaitCommandRunner(), - new WindowCommandRunner(), - ], - new CommandPreprocessor() - )); + $this->validators[$className] = new ValidCommandValidator(new CommandManager(new MockHttpClient())); } return parent::getInstance($constraint); diff --git a/tests/Generator/GeneratorManagerTest.php b/tests/Generator/GeneratorManagerTest.php index 03ce83b1..999b8d6f 100644 --- a/tests/Generator/GeneratorManagerTest.php +++ b/tests/Generator/GeneratorManagerTest.php @@ -2,54 +2,34 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Generator; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ServiceLocator; -use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; use Tienvx\Bundle\MbtBundle\Generator\GeneratorInterface; use Tienvx\Bundle\MbtBundle\Generator\GeneratorManager; +use Tienvx\Bundle\MbtBundle\Plugin\PluginInterface; +use Tienvx\Bundle\MbtBundle\Plugin\PluginManagerInterface; +use Tienvx\Bundle\MbtBundle\Tests\Plugin\PluginManagerTest; /** * @covers \Tienvx\Bundle\MbtBundle\Generator\GeneratorManager - * @covers \Tienvx\Bundle\MbtBundle\Plugin\AbstractPluginManager + * + * @uses \Tienvx\Bundle\MbtBundle\Plugin\PluginManager */ -class GeneratorManagerTest extends TestCase +class GeneratorManagerTest extends PluginManagerTest { - protected GeneratorManager $generatorManager; - protected GeneratorInterface $generator; - protected ServiceLocator $locator; + protected array $plugins = ['random']; + protected string $getMethod = 'getGenerator'; - protected function setUp(): void + protected function createPluginManager(): PluginManagerInterface { - $this->generator = $this->createMock(GeneratorInterface::class); - $this->locator = $this->createMock(ServiceLocator::class); - $plugins = ['random']; - $this->generatorManager = new GeneratorManager($this->locator, $plugins); + return new GeneratorManager($this->locator, $this->plugins); } - public function testGet() + protected function createPlugin(): PluginInterface { - $this->locator->expects($this->once())->method('has')->with('random')->willReturn(true); - $this->locator->expects($this->once())->method('get')->with('random')->willReturn($this->generator); - $this->assertSame($this->generator, $this->generatorManager->get('random')); + return $this->createMock(GeneratorInterface::class); } - public function testAll() + protected function getInvalidPluginExceptionMessage(string $plugin): string { - $this->assertSame(['random'], $this->generatorManager->all()); - } - - public function testHasRandom() - { - $this->locator->expects($this->once())->method('has')->with('random')->willReturn(true); - $this->assertTrue($this->generatorManager->has('random')); - } - - public function testDoesNotHaveOther(): void - { - $this->locator->expects($this->never())->method('has'); - $this->assertFalse($this->generatorManager->has('other')); - $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('Generator "other" does not exist.'); - $this->generatorManager->getGenerator('other'); + return sprintf('Generator "%s" does not exist.', $plugin); } } diff --git a/tests/Generator/RandomGeneratorTest.php b/tests/Generator/RandomGeneratorTest.php index ee821b94..8f51bcbf 100644 --- a/tests/Generator/RandomGeneratorTest.php +++ b/tests/Generator/RandomGeneratorTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use SingleColorPetrinet\Model\ColorfulFactory; use SingleColorPetrinet\Service\GuardedTransitionService; +use Tienvx\AssignmentsEvaluator\AssignmentsEvaluator; use Tienvx\Bundle\MbtBundle\Entity\Model\Revision; use Tienvx\Bundle\MbtBundle\Entity\Task; use Tienvx\Bundle\MbtBundle\Generator\GeneratorManager; @@ -45,7 +46,8 @@ protected function setUp(): void { $factory = new ColorfulFactory(); $expressionLanguage = new ExpressionLanguage(); - $petrinetHelper = new PetrinetHelper($factory, $expressionLanguage); + $assignmentsEvaluator = new AssignmentsEvaluator($expressionLanguage); + $petrinetHelper = new PetrinetHelper($factory, $expressionLanguage, $assignmentsEvaluator); $markingHelper = new MarkingHelper($factory); $modelHelper = new ModelHelper(); $transitionService = new GuardedTransitionService($factory); diff --git a/tests/MessageHandler/CreateBugMessageHandlerTest.php b/tests/MessageHandler/CreateBugMessageHandlerTest.php new file mode 100644 index 00000000..df6ff9c9 --- /dev/null +++ b/tests/MessageHandler/CreateBugMessageHandlerTest.php @@ -0,0 +1,77 @@ +config = $this->createMock(ConfigInterface::class); + $this->taskRepository = $this->createMock(TaskRepositoryInterface::class); + $this->handler = new CreateBugMessageHandler($this->config, $this->taskRepository); + $this->message = new CreateBugMessage( + 123, + [$this->createMock(StepInterface::class), $this->createMock(StepInterface::class)], + 'Something wrong' + ); + $this->task = new Task(); + $this->task->setId(123); + } + + public function testInvokeNoTask(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Can not create bug for task 123: task not found'); + $this->taskRepository->expects($this->once())->method('find')->with(123)->willReturn(null); + call_user_func($this->handler, $this->message); + } + + public function testInvoke(): void + { + $this->taskRepository + ->expects($this->once()) + ->method('find') + ->with(123) + ->willReturn($this->task); + $this->config + ->expects($this->once()) + ->method('getDefaultBugTitle') + ->willReturn('Bug title'); + $this->taskRepository + ->expects($this->once()) + ->method('addBug') + ->with($this->task, $this->callback(function ($bug) { + return $bug instanceof Bug && + 'Bug title' === $bug->getTitle() && + $bug->getSteps() === $this->message->getSteps() && + 'Something wrong' === $bug->getMessage(); + })); + call_user_func($this->handler, $this->message); + } +} diff --git a/tests/MessageHandler/RecordVideoMessageHandlerTest.php b/tests/MessageHandler/RecordVideoMessageHandlerTest.php index 9b5fc94e..84418553 100644 --- a/tests/MessageHandler/RecordVideoMessageHandlerTest.php +++ b/tests/MessageHandler/RecordVideoMessageHandlerTest.php @@ -2,6 +2,7 @@ namespace Tienvx\Bundle\MbtBundle\Tests\MessageHandler; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Tienvx\Bundle\MbtBundle\Message\RecordVideoMessage; use Tienvx\Bundle\MbtBundle\MessageHandler\RecordVideoMessageHandler; @@ -9,24 +10,24 @@ /** * @covers \Tienvx\Bundle\MbtBundle\MessageHandler\RecordVideoMessageHandler - * - * @uses \Tienvx\Bundle\MbtBundle\Message\RecordVideoMessage + * @covers \Tienvx\Bundle\MbtBundle\Message\RecordVideoMessage */ class RecordVideoMessageHandlerTest extends TestCase { - protected BugHelperInterface $bugHelper; + protected BugHelperInterface|MockObject $bugHelper; protected RecordVideoMessageHandler $handler; + protected RecordVideoMessage $message; protected function setUp(): void { $this->bugHelper = $this->createMock(BugHelperInterface::class); $this->handler = new RecordVideoMessageHandler($this->bugHelper); + $this->message = new RecordVideoMessage(123); } public function testInvoke(): void { $this->bugHelper->expects($this->once())->method('recordVideo')->with(123); - $message = new RecordVideoMessage(123); - call_user_func($this->handler, $message); + call_user_func($this->handler, $this->message); } } diff --git a/tests/MessageHandler/ReduceBugMessageHandlerTest.php b/tests/MessageHandler/ReduceBugMessageHandlerTest.php index bf314e54..8d5c6acf 100644 --- a/tests/MessageHandler/ReduceBugMessageHandlerTest.php +++ b/tests/MessageHandler/ReduceBugMessageHandlerTest.php @@ -2,6 +2,7 @@ namespace Tienvx\Bundle\MbtBundle\Tests\MessageHandler; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Tienvx\Bundle\MbtBundle\Message\ReduceBugMessage; use Tienvx\Bundle\MbtBundle\MessageHandler\ReduceBugMessageHandler; @@ -9,24 +10,24 @@ /** * @covers \Tienvx\Bundle\MbtBundle\MessageHandler\ReduceBugMessageHandler - * - * @uses \Tienvx\Bundle\MbtBundle\Message\ReduceBugMessage + * @covers \Tienvx\Bundle\MbtBundle\Message\ReduceBugMessage */ class ReduceBugMessageHandlerTest extends TestCase { - protected BugHelperInterface $bugHelper; + protected BugHelperInterface|MockObject $bugHelper; protected ReduceBugMessageHandler $handler; + protected ReduceBugMessage $message; protected function setUp(): void { $this->bugHelper = $this->createMock(BugHelperInterface::class); $this->handler = new ReduceBugMessageHandler($this->bugHelper); + $this->message = new ReduceBugMessage(123); } public function testInvoke(): void { $this->bugHelper->expects($this->once())->method('reduceBug')->with(123); - $message = new ReduceBugMessage(123); - call_user_func($this->handler, $message); + call_user_func($this->handler, $this->message); } } diff --git a/tests/MessageHandler/ReduceStepsMessageHandlerTest.php b/tests/MessageHandler/ReduceStepsMessageHandlerTest.php index 2c161caf..3a45a087 100644 --- a/tests/MessageHandler/ReduceStepsMessageHandlerTest.php +++ b/tests/MessageHandler/ReduceStepsMessageHandlerTest.php @@ -2,6 +2,7 @@ namespace Tienvx\Bundle\MbtBundle\Tests\MessageHandler; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Tienvx\Bundle\MbtBundle\Message\ReduceStepsMessage; use Tienvx\Bundle\MbtBundle\MessageHandler\ReduceStepsMessageHandler; @@ -9,24 +10,24 @@ /** * @covers \Tienvx\Bundle\MbtBundle\MessageHandler\ReduceStepsMessageHandler - * - * @uses \Tienvx\Bundle\MbtBundle\Message\ReduceStepsMessage + * @covers \Tienvx\Bundle\MbtBundle\Message\ReduceStepsMessage */ class ReduceStepsMessageHandlerTest extends TestCase { - protected BugHelperInterface $bugHelper; + protected BugHelperInterface|MockObject $bugHelper; protected ReduceStepsMessageHandler $handler; + protected ReduceStepsMessage $message; protected function setUp(): void { $this->bugHelper = $this->createMock(BugHelperInterface::class); $this->handler = new ReduceStepsMessageHandler($this->bugHelper); + $this->message = new ReduceStepsMessage(123, 6, 1, 2); } public function testInvoke(): void { $this->bugHelper->expects($this->once())->method('reduceSteps')->with(123, 6, 1, 2); - $message = new ReduceStepsMessage(123, 6, 1, 2); - call_user_func($this->handler, $message); + call_user_func($this->handler, $this->message); } } diff --git a/tests/MessageHandler/ReportBugMessageHandlerTest.php b/tests/MessageHandler/ReportBugMessageHandlerTest.php index 7015aff3..04facf6a 100644 --- a/tests/MessageHandler/ReportBugMessageHandlerTest.php +++ b/tests/MessageHandler/ReportBugMessageHandlerTest.php @@ -2,6 +2,7 @@ namespace Tienvx\Bundle\MbtBundle\Tests\MessageHandler; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Tienvx\Bundle\MbtBundle\Message\ReportBugMessage; use Tienvx\Bundle\MbtBundle\MessageHandler\ReportBugMessageHandler; @@ -9,24 +10,24 @@ /** * @covers \Tienvx\Bundle\MbtBundle\MessageHandler\ReportBugMessageHandler - * - * @uses \Tienvx\Bundle\MbtBundle\Message\ReportBugMessage + * @covers \Tienvx\Bundle\MbtBundle\Message\ReportBugMessage */ class ReportBugMessageHandlerTest extends TestCase { - protected BugHelperInterface $bugHelper; + protected BugHelperInterface|MockObject $bugHelper; protected ReportBugMessageHandler $handler; + protected ReportBugMessage $message; protected function setUp(): void { $this->bugHelper = $this->createMock(BugHelperInterface::class); $this->handler = new ReportBugMessageHandler($this->bugHelper); + $this->message = new ReportBugMessage(123); } public function testInvoke(): void { $this->bugHelper->expects($this->once())->method('reportBug')->with(123); - $message = new ReportBugMessage(123); - call_user_func($this->handler, $message); + call_user_func($this->handler, $this->message); } } diff --git a/tests/MessageHandler/RunTaskMessageHandlerTest.php b/tests/MessageHandler/RunTaskMessageHandlerTest.php index 0895872e..06bbe563 100644 --- a/tests/MessageHandler/RunTaskMessageHandlerTest.php +++ b/tests/MessageHandler/RunTaskMessageHandlerTest.php @@ -2,33 +2,32 @@ namespace Tienvx\Bundle\MbtBundle\Tests\MessageHandler; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; use Tienvx\Bundle\MbtBundle\Message\RunTaskMessage; use Tienvx\Bundle\MbtBundle\MessageHandler\RunTaskMessageHandler; use Tienvx\Bundle\MbtBundle\Service\Task\TaskHelperInterface; -use Tienvx\Bundle\MbtBundle\Tests\StepsTestCase; /** * @covers \Tienvx\Bundle\MbtBundle\MessageHandler\RunTaskMessageHandler - * - * @uses \Tienvx\Bundle\MbtBundle\Message\RunTaskMessage + * @covers \Tienvx\Bundle\MbtBundle\Message\RunTaskMessage */ -class RunTaskMessageHandlerTest extends StepsTestCase +class RunTaskMessageHandlerTest extends TestCase { - protected TaskHelperInterface $taskHelper; + protected TaskHelperInterface|MockObject $taskHelper; protected RunTaskMessageHandler $handler; + protected RunTaskMessage $message; protected function setUp(): void { $this->taskHelper = $this->createMock(TaskHelperInterface::class); - $this->handler = new RunTaskMessageHandler( - $this->taskHelper - ); + $this->handler = new RunTaskMessageHandler($this->taskHelper); + $this->message = new RunTaskMessage(123); } public function testInvoke(): void { $this->taskHelper->expects($this->once())->method('run')->with(123); - $message = new RunTaskMessage(123); - call_user_func($this->handler, $message); + call_user_func($this->handler, $this->message); } } diff --git a/tests/Model/Bug/StepTest.php b/tests/Model/Bug/StepTest.php index dedaee59..0e0b7a70 100644 --- a/tests/Model/Bug/StepTest.php +++ b/tests/Model/Bug/StepTest.php @@ -4,9 +4,10 @@ use PHPUnit\Framework\TestCase; use SingleColorPetrinet\Model\Color; +use SingleColorPetrinet\Model\ColorInterface; +use Tienvx\Bundle\MbtBundle\Model\Bug\Step; use Tienvx\Bundle\MbtBundle\Model\Bug\StepInterface; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\CommandInterface; -use Tienvx\Bundle\MbtBundle\ValueObject\Bug\Step; /** * @covers \Tienvx\Bundle\MbtBundle\Model\Bug\Step @@ -16,25 +17,62 @@ class StepTest extends TestCase protected StepInterface $step; protected CommandInterface $command1; protected CommandInterface $command2; + protected array $places = [0 => 1, 1 => 2]; + protected ColorInterface $color; + protected int $transition = 123; protected function setUp(): void { - $this->step = new Step([1, 2], new Color(['key' => 'value']), 123); + $this->color = new Color(['key' => 'value']); + $this->step = $this->createStep(); } public function testSerialize(): void { + $className = get_class($this->step); // phpcs:ignore Generic.Files.LineLength - $this->assertSame('O:44:"Tienvx\Bundle\MbtBundle\ValueObject\Bug\Step":3:{s:5:"color";a:1:{s:3:"key";s:5:"value";}s:6:"places";a:2:{i:0;i:1;i:1;i:2;}s:10:"transition";i:123;}', serialize($this->step)); + $this->assertSame('O:' . strlen($className) . ':"' . $className . '":3:{s:5:"color";a:1:{s:3:"key";s:5:"value";}s:6:"places";a:2:{i:0;i:1;i:1;i:2;}s:10:"transition";i:123;}', serialize($this->step)); } public function testUnerialize(): void { + $className = get_class($this->step); // phpcs:ignore Generic.Files.LineLength - $step = unserialize('O:44:"Tienvx\Bundle\MbtBundle\ValueObject\Bug\Step":3:{s:5:"color";a:1:{s:4:"key1";s:6:"value2";}s:6:"places";a:2:{i:0;i:3;i:1;i:4;}s:10:"transition";i:234;}'); + $step = unserialize('O:' . strlen($className) . ':"' . $className . '":3:{s:5:"color";a:1:{s:4:"key1";s:6:"value2";}s:6:"places";a:2:{i:0;i:3;i:1;i:4;}s:10:"transition";i:234;}'); $this->assertInstanceOf(StepInterface::class, $step); $this->assertSame(['key1' => 'value2'], $step->getColor()->getValues()); $this->assertSame([3, 4], $step->getPlaces()); $this->assertSame(234, $step->getTransition()); } + + public function testClone(): void + { + $step = clone $this->step; + $this->assertNotSame($this->step->getColor(), $step->getColor()); + $this->assertSame($this->step->getColor()->getValues(), $step->getColor()->getValues()); + } + + /** + * @dataProvider nodeIdProvider + */ + public function testGetUniqueNodeId(?array $places, string $id): void + { + if ($places) { + $this->step->setPlaces($places); + } + $this->assertSame($id, $this->step->getUniqueNodeId()); + } + + public function nodeIdProvider(): array + { + return [ + [null, '89fefb193877ee62e29d1da5975dcc47'], + [[0 => 2, 1 => 1], '02878487ecf2302bf7ba2cc919514889'], + ]; + } + + protected function createStep(): StepInterface + { + return new Step($this->places, $this->color, $this->transition); + } } diff --git a/tests/Model/Bug/VideoTest.php b/tests/Model/Bug/VideoTest.php new file mode 100644 index 00000000..d9a1646b --- /dev/null +++ b/tests/Model/Bug/VideoTest.php @@ -0,0 +1,39 @@ +video = $this->createVideo(); + } + + public function testRecording(): void + { + $this->assertFalse($this->video->isRecording()); + $this->video->setRecording(true); + $this->assertTrue($this->video->isRecording()); + } + + public function testErrorMessage(): void + { + $this->assertNull($this->video->getErrorMessage()); + $this->video->setErrorMessage('Something wrong'); + $this->assertSame('Something wrong', $this->video->getErrorMessage()); + } + + protected function createVideo(): VideoInterface + { + return new Video(); + } +} diff --git a/tests/Model/BugTest.php b/tests/Model/BugTest.php new file mode 100644 index 00000000..02cc2403 --- /dev/null +++ b/tests/Model/BugTest.php @@ -0,0 +1,83 @@ +steps = [ + new Step([], new Color(), 0), + new Step([], new Color(), 1), + ]; + $this->task = new Task(); + $this->progress = new Progress(); + $this->video = new Video(); + $this->bug = $this->createBug(); + $this->bug->setId(123); + $this->bug->setTitle('bug title'); + $this->bug->setSteps($this->steps); + $this->bug->setTask($this->task); + $this->bug->setMessage('bug message'); + $this->bug->setProgress($this->progress); + $this->bug->setVideo($this->video); + $this->bug->setClosed(true); + $this->bug->setDebug(true); + } + + public function testProperties(): void + { + $this->assertSame(123, $this->bug->getId()); + $this->assertSame('bug title', $this->bug->getTitle()); + $this->assertSame($this->steps, $this->bug->getSteps()); + $this->assertSame($this->task, $this->bug->getTask()); + $this->assertSame('bug message', $this->bug->getMessage()); + $this->assertSame($this->progress, $this->bug->getProgress()); + $this->assertSame(true, $this->bug->isClosed()); + $this->assertSame($this->video, $this->bug->getVideo()); + $this->assertSame(true, $this->bug->isDebug()); + } + + public function testGetLogName(): void + { + $this->assertSame('bug-123.log', $this->bug->getLogName()); + } + + public function testGetVideoName(): void + { + $this->assertSame('bug-123.mp4', $this->bug->getVideoName()); + } + + protected function createBug(): BugInterface + { + return new Bug(); + } +} diff --git a/tests/Model/Generator/StateTest.php b/tests/Model/Generator/StateTest.php new file mode 100644 index 00000000..c1701da3 --- /dev/null +++ b/tests/Model/Generator/StateTest.php @@ -0,0 +1,53 @@ +state = new State([], [], 5, 10); + } + + /** + * @dataProvider totalPlacesAndTransitionsProvider + */ + public function testMissingPlaceOrTransition(int $places, int $transitions): void + { + $this->expectExceptionObject(new OutOfRangeException('State need at least 1 place and 1 transition')); + new State([], [], $places, $transitions); + } + + public function testPlaceCoverage(): void + { + $this->assertSame(0.0, $this->state->getPlaceCoverage()); + $this->state->addVisitedPlace(1); + $this->assertSame(20.0, $this->state->getPlaceCoverage()); + } + + public function testTransitionCoverage(): void + { + $this->assertSame(0.0, $this->state->getTransitionCoverage()); + $this->state->addVisitedTransition(1); + $this->assertSame(10.0, $this->state->getTransitionCoverage()); + } + + public function totalPlacesAndTransitionsProvider(): array + { + return [ + [0, 11], + [12, 0], + [0, 0], + ]; + } +} diff --git a/tests/Model/Model/Revision/CommandTest.php b/tests/Model/Model/Revision/CommandTest.php index 82e6df46..6c9e8202 100644 --- a/tests/Model/Model/Revision/CommandTest.php +++ b/tests/Model/Model/Revision/CommandTest.php @@ -3,8 +3,6 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Model\Model\Revision; use PHPUnit\Framework\TestCase; -use Tienvx\Bundle\MbtBundle\Command\Runner\MouseCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\WindowCommandRunner; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\CommandInterface; @@ -17,25 +15,32 @@ class CommandTest extends TestCase protected function setUp(): void { - $this->command = new Command(); - $this->command->setCommand(WindowCommandRunner::OPEN); + $this->command = $this->createCommand(); + $this->command->setCommand('open'); $this->command->setTarget('http://localhost:1234'); $this->command->setValue('123'); } public function testSerialize(): void { + $className = get_class($this->command); // phpcs:ignore Generic.Files.LineLength - $this->assertSame('O:52:"Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command":3:{s:7:"command";s:4:"open";s:6:"target";s:21:"http://localhost:1234";s:5:"value";s:3:"123";}', serialize($this->command)); + $this->assertSame('O:' . strlen($className) . ':"' . $className . '":3:{s:7:"command";s:4:"open";s:6:"target";s:21:"http://localhost:1234";s:5:"value";s:3:"123";}', serialize($this->command)); } public function testUnerialize(): void { + $className = get_class($this->command); // phpcs:ignore Generic.Files.LineLength - $command = unserialize('O:52:"Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command":3:{s:7:"command";s:5:"click";s:6:"target";s:11:"css=.button";s:5:"value";N;}'); + $command = unserialize('O:' . strlen($className) . ':"' . $className . '":3:{s:7:"command";s:5:"click";s:6:"target";s:11:"css=.button";s:5:"value";N;}'); $this->assertInstanceOf(CommandInterface::class, $command); - $this->assertSame(MouseCommandRunner::CLICK, $command->getCommand()); + $this->assertSame('click', $command->getCommand()); $this->assertSame('css=.button', $command->getTarget()); $this->assertSame(null, $command->getValue()); } + + protected function createCommand(): CommandInterface + { + return new Command(); + } } diff --git a/tests/Model/Model/Revision/PlaceTest.php b/tests/Model/Model/Revision/PlaceTest.php index be8e22d9..86bc4258 100644 --- a/tests/Model/Model/Revision/PlaceTest.php +++ b/tests/Model/Model/Revision/PlaceTest.php @@ -3,7 +3,6 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Model\Model\Revision; use PHPUnit\Framework\TestCase; -use Tienvx\Bundle\MbtBundle\Command\Runner\AssertionRunner; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\CommandInterface; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\Place; @@ -24,7 +23,8 @@ class PlaceTest extends TestCase protected function setUp(): void { $this->setUpCommands(); - $this->place = new Place(); + $this->place = $this->createPlace(); + $this->place->setLabel('place label'); $this->place->setCommands([ $this->command1, $this->command2, @@ -35,29 +35,36 @@ protected function setUpCommands(): void { $this->command1 = new Command(); $this->command2 = new Command(); - $this->command1->setCommand(AssertionRunner::ASSERT_TEXT); + $this->command1->setCommand('assertText'); $this->command1->setTarget('css=.title'); $this->command1->setValue('Hello'); - $this->command2->setCommand(AssertionRunner::ASSERT_ALERT); + $this->command2->setCommand('assertAlert'); $this->command2->setTarget('css=.warning'); $this->command2->setValue('Are you sure?'); } public function testSerialize(): void { + $className = get_class($this->place); // phpcs:ignore Generic.Files.LineLength - $this->assertSame('O:50:"Tienvx\Bundle\MbtBundle\Model\Model\Revision\Place":2:{s:5:"label";s:0:"";s:8:"commands";a:2:{i:0;a:3:{s:7:"command";s:10:"assertText";s:6:"target";s:10:"css=.title";s:5:"value";s:5:"Hello";}i:1;a:3:{s:7:"command";s:11:"assertAlert";s:6:"target";s:12:"css=.warning";s:5:"value";s:13:"Are you sure?";}}}', serialize($this->place)); + $this->assertSame('O:' . strlen($className) . ':"' . $className . '":2:{s:5:"label";s:11:"place label";s:8:"commands";a:2:{i:0;a:3:{s:7:"command";s:10:"assertText";s:6:"target";s:10:"css=.title";s:5:"value";s:5:"Hello";}i:1;a:3:{s:7:"command";s:11:"assertAlert";s:6:"target";s:12:"css=.warning";s:5:"value";s:13:"Are you sure?";}}}', serialize($this->place)); } public function testUnerialize(): void { + $className = get_class($this->place); // phpcs:ignore Generic.Files.LineLength - $place = unserialize('O:50:"Tienvx\Bundle\MbtBundle\Model\Model\Revision\Place":2:{s:5:"label";s:10:"Serialized";s:8:"commands";a:1:{i:0;a:3:{s:7:"command";s:19:"assertSelectedValue";s:6:"target";s:12:"css=.country";s:5:"value";s:2:"vn";}}}'); + $place = unserialize('O:' . strlen($className) . ':"' . $className . '":2:{s:5:"label";s:10:"Serialized";s:8:"commands";a:1:{i:0;a:3:{s:7:"command";s:19:"assertSelectedValue";s:6:"target";s:12:"css=.country";s:5:"value";s:2:"vn";}}}'); $this->assertInstanceOf(PlaceInterface::class, $place); $this->assertSame('Serialized', $place->getLabel()); $this->assertInstanceOf(CommandInterface::class, $place->getCommands()[0]); - $this->assertSame(AssertionRunner::ASSERT_SELECTED_VALUE, $place->getCommands()[0]->getCommand()); + $this->assertSame('assertSelectedValue', $place->getCommands()[0]->getCommand()); $this->assertSame('css=.country', $place->getCommands()[0]->getTarget()); $this->assertSame('vn', $place->getCommands()[0]->getValue()); } + + protected function createPlace(): PlaceInterface + { + return new Place(); + } } diff --git a/tests/Model/Model/Revision/TransitionTest.php b/tests/Model/Model/Revision/TransitionTest.php index 60d3f92b..51a98084 100644 --- a/tests/Model/Model/Revision/TransitionTest.php +++ b/tests/Model/Model/Revision/TransitionTest.php @@ -3,9 +3,6 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Model\Model\Revision; use PHPUnit\Framework\TestCase; -use Tienvx\Bundle\MbtBundle\Command\Runner\KeyboardCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\MouseCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\StoreCommandRunner; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\CommandInterface; use Tienvx\Bundle\MbtBundle\Model\Model\Revision\Transition; @@ -26,8 +23,10 @@ class TransitionTest extends TestCase protected function setUp(): void { $this->setUpCommands(); - $this->transition = new Transition(); + $this->transition = $this->createTransition(); + $this->transition->setLabel('transition label'); $this->transition->setGuard('count > 2'); + $this->transition->setExpression('count = count + 1'); $this->transition->setFromPlaces([1, 2, 3]); $this->transition->setToPlaces([12, 23]); $this->transition->setCommands([ @@ -40,32 +39,48 @@ protected function setUpCommands(): void { $this->command1 = new Command(); $this->command2 = new Command(); - $this->command1->setCommand(KeyboardCommandRunner::TYPE); + $this->command1->setCommand('type'); $this->command1->setTarget('css=.email'); $this->command1->setValue('test@example.com'); - $this->command2->setCommand(MouseCommandRunner::CLICK); + $this->command2->setCommand('click'); $this->command2->setTarget('css=.link'); $this->command2->setValue(null); } + public function testStartTransition(): void + { + $this->assertFalse($this->transition->isStart()); + $this->transition->setFromPlaces([]); + $this->assertTrue($this->transition->isStart()); + } + public function testSerialize(): void { + $className = get_class($this->transition); // phpcs:ignore Generic.Files.LineLength - $this->assertSame('O:55:"Tienvx\Bundle\MbtBundle\Model\Model\Revision\Transition":5:{s:5:"label";s:0:"";s:5:"guard";s:9:"count > 2";s:10:"fromPlaces";a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}s:8:"toPlaces";a:2:{i:0;i:12;i:1;i:23;}s:8:"commands";a:2:{i:0;a:3:{s:7:"command";s:4:"type";s:6:"target";s:10:"css=.email";s:5:"value";s:16:"test@example.com";}i:1;a:3:{s:7:"command";s:5:"click";s:6:"target";s:9:"css=.link";s:5:"value";N;}}}', serialize($this->transition)); + $this->assertSame('O:' . strlen($className) . ':"' . $className . '":6:{s:5:"label";s:16:"transition label";s:5:"guard";s:9:"count > 2";s:10:"expression";s:17:"count = count + 1";s:10:"fromPlaces";a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}s:8:"toPlaces";a:2:{i:0;i:12;i:1;i:23;}s:8:"commands";a:2:{i:0;a:3:{s:7:"command";s:4:"type";s:6:"target";s:10:"css=.email";s:5:"value";s:16:"test@example.com";}i:1;a:3:{s:7:"command";s:5:"click";s:6:"target";s:9:"css=.link";s:5:"value";N;}}}', serialize($this->transition)); } public function testUnerialize(): void { + $className = get_class($this->transition); // phpcs:ignore Generic.Files.LineLength - $transition = unserialize('O:55:"Tienvx\Bundle\MbtBundle\Model\Model\Revision\Transition":5:{s:5:"label";s:10:"Serialized";s:5:"guard";s:10:"count == 3";s:10:"fromPlaces";a:2:{i:0;i:1;i:1;i:4;}s:8:"toPlaces";a:1:{i:0;i:15;}s:8:"commands";a:1:{i:0;a:3:{s:7:"command";s:5:"store";s:6:"target";s:2:"55";s:5:"value";s:6:"number";}}}'); + $transition = unserialize('O:' . strlen($className) . ':"' . $className . '":6:{s:5:"label";s:10:"Serialized";s:5:"guard";s:10:"count == 3";s:10:"expression";s:17:"count = count - 1";s:10:"fromPlaces";a:2:{i:0;i:1;i:1;i:4;}s:8:"toPlaces";a:1:{i:0;i:15;}s:8:"commands";a:1:{i:0;a:3:{s:7:"command";s:5:"store";s:6:"target";s:2:"55";s:5:"value";s:6:"number";}}}'); $this->assertInstanceOf(TransitionInterface::class, $transition); $this->assertSame('Serialized', $transition->getLabel()); $this->assertSame('count == 3', $transition->getGuard()); + $this->assertSame('count = count - 1', $transition->getExpression()); $this->assertSame([1, 4], $transition->getFromPlaces()); $this->assertSame([15], $transition->getToPlaces()); $this->assertInstanceOf(CommandInterface::class, $transition->getCommands()[0]); - $this->assertSame(StoreCommandRunner::STORE, $transition->getCommands()[0]->getCommand()); + $this->assertSame('store', $transition->getCommands()[0]->getCommand()); $this->assertSame('55', $transition->getCommands()[0]->getTarget()); $this->assertSame('number', $transition->getCommands()[0]->getValue()); + $this->assertFalse($transition->isStart()); + } + + protected function createTransition(): TransitionInterface + { + return new Transition(); } } diff --git a/tests/Model/Model/RevisionTest.php b/tests/Model/Model/RevisionTest.php new file mode 100644 index 00000000..20ab245b --- /dev/null +++ b/tests/Model/Model/RevisionTest.php @@ -0,0 +1,179 @@ +revision = $this->createRevision(); + $this->revision->setId(1); + $this->places = [ + $p1 = new Place(), + $p2 = new Place(), + ]; + $p1->setLabel(''); + $p1->setCommands([ + $c1 = new Command(), + $c2 = new Command(), + ]); + $c1->setCommand(''); + $c1->setTarget('css=.name'); + $c1->setValue('test'); + $c2->setCommand('click'); + $c2->setTarget(null); + $c2->setValue('test'); + $p2->setLabel('p2'); + $p2->setCommands([ + $c3 = new Command(), + $c4 = new Command(), + ]); + $c3->setCommand('doNoThing'); + $c3->setTarget('css=.about'); + $c3->setValue('test'); + $c4->setCommand('clickAt'); + $c4->setTarget('css=.avatar'); + $c4->setValue(null); + $this->revision->setPlaces($this->places); + $this->transitions = [ + $t1 = new Transition(), + $t2 = new Transition(), + ]; + $t1->setLabel('t1'); + $t1->setFromPlaces([1]); + $t1->setToPlaces([1, 2]); + $t1->setExpression('count = count + 1'); + $t2->setLabel(''); + $t2->setFromPlaces([1, 2]); + $t2->setToPlaces([]); + $t2->setGuard('count > 1'); + $this->revision->setTransitions($this->transitions); + $this->model = new Model(); + $this->model->setActiveRevision(new Revision()); + $this->revision->setModel($this->model); + } + + public function testProperties(): void + { + $this->assertSame(1, $this->revision->getId()); + $this->assertSame($this->model, $this->revision->getModel()); + $this->assertSame($this->places, $this->revision->getPlaces()); + $this->assertSame($this->places[0], $this->revision->getPlace(0)); + $this->assertSame($this->transitions, $this->revision->getTransitions()); + $this->assertSame($this->transitions[0], $this->revision->getTransition(0)); + } + + public function testConvertToString(): void + { + $this->assertSame('', (string) $this->revision); + $this->model->setLabel('model label'); + $this->assertSame('model label', (string) $this->revision); + } + + public function testIsLatest(): void + { + $this->assertFalse($this->revision->isLatest()); + $this->model->setActiveRevision($this->revision); + $this->assertTrue($this->revision->isLatest()); + } + + public function testToArray(): void + { + $this->assertSame([ + 'places' => [ + 0 => [ + 'label' => '', + 'commands' => [ + 0 => [ + 'command' => '', + 'target' => 'css=.name', + 'value' => 'test', + ], + 1 => [ + 'command' => 'click', + 'target' => null, + 'value' => 'test', + ], + ], + ], + 1 => [ + 'label' => 'p2', + 'commands' => [ + 0 => [ + 'command' => 'doNoThing', + 'target' => 'css=.about', + 'value' => 'test', + ], + 1 => [ + 'command' => 'clickAt', + 'target' => 'css=.avatar', + 'value' => null, + ], + ], + ], + ], + 'transitions' => [ + 0 => [ + 'label' => 't1', + 'guard' => null, + 'expression' => 'count = count + 1', + 'fromPlaces' => [ + 0 => 1, + ], + 'toPlaces' => [ + 0 => 1, + 1 => 2, + ], + 'commands' => [ + ], + ], + 1 => [ + 'label' => '', + 'guard' => 'count > 1', + 'expression' => null, + 'fromPlaces' => [ + 0 => 1, + 1 => 2, + ], + 'toPlaces' => [ + ], + 'commands' => [ + ], + ], + ], + ], $this->revision->toArray()); + } + + protected function createRevision(): RevisionInterface + { + return new Revision(); + } +} diff --git a/tests/Model/ModelTest.php b/tests/Model/ModelTest.php new file mode 100644 index 00000000..5d535552 --- /dev/null +++ b/tests/Model/ModelTest.php @@ -0,0 +1,82 @@ +revision = new Revision(); + $this->revision->setId(1); + $this->model = $this->createModel(); + $this->model->setId(1); + $this->model->setLabel(''); + $this->model->setTags('tag1,tag1,tag2,,tag3'); + $this->model->setAuthor(12); + $this->model->setActiveRevision($this->revision); + } + + public function testProperties(): void + { + $this->assertSame(1, $this->model->getId()); + $this->assertSame('', $this->model->getLabel()); + $this->assertSame('tag1,tag1,tag2,,tag3', $this->model->getTags()); + $this->assertSame(12, $this->model->getAuthor()); + } + + public function testActiveRevision(): void + { + $this->assertSame($this->revision, $this->model->getActiveRevision()); + $revision = new Revision(); + $revision->setId(2); + $this->model->setActiveRevision($revision); + $this->assertSame($revision, $this->model->getActiveRevision()); + } + + public function testAddRemoveRevision(): void + { + $this->assertTrue($this->model->getRevisions()->contains($this->revision)); + $this->assertCount(1, $this->model->getRevisions()); + $revision = new Revision(); + $this->model->addRevision($revision); + $this->assertTrue($this->model->getRevisions()->contains($revision)); + $this->assertCount(2, $this->model->getRevisions()); + $this->model->removeRevision($this->revision); + $this->assertFalse($this->model->getRevisions()->contains($this->revision)); + $this->assertCount(1, $this->model->getRevisions()); + $this->model->removeRevision($revision); + $this->assertFalse($this->model->getRevisions()->contains($revision)); + $this->assertCount(0, $this->model->getRevisions()); + } + + public function testToArray(): void + { + $this->assertSame([ + 'label' => '', + 'tags' => 'tag1,tag1,tag2,,tag3', + 'places' => [], + 'transitions' => [], + ], $this->model->toArray()); + } + + protected function createModel(): ModelInterface + { + return new Model(); + } +} diff --git a/tests/Model/ProgressTest.php b/tests/Model/ProgressTest.php index 41e38013..600dffca 100644 --- a/tests/Model/ProgressTest.php +++ b/tests/Model/ProgressTest.php @@ -3,7 +3,7 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Model; use PHPUnit\Framework\TestCase; -use Tienvx\Bundle\MbtBundle\Entity\Progress; +use Tienvx\Bundle\MbtBundle\Model\Progress; use Tienvx\Bundle\MbtBundle\Model\ProgressInterface; /** @@ -15,7 +15,7 @@ class ProgressTest extends TestCase protected function setUp(): void { - $this->progress = new Progress(); + $this->progress = $this->createProgress(); $this->progress->setTotal(10); $this->progress->setProcessed(5); } @@ -40,4 +40,9 @@ public function testSetTotal(): void $this->assertSame(5, $this->progress->getProcessed()); $this->assertSame(15, $this->progress->getTotal()); } + + protected function createProgress(): ProgressInterface + { + return new Progress(); + } } diff --git a/tests/Model/Task/BrowserTest.php b/tests/Model/Task/BrowserTest.php new file mode 100644 index 00000000..d9d2ece6 --- /dev/null +++ b/tests/Model/Task/BrowserTest.php @@ -0,0 +1,37 @@ +browser = $this->createBrowser(); + } + + public function testName(): void + { + $this->browser->setName('firefox'); + $this->assertSame('firefox', $this->browser->getName()); + } + + public function testVersion(): void + { + $this->browser->setVersion('99.0'); + $this->assertSame('99.0', $this->browser->getVersion()); + } + + protected function createBrowser(): BrowserInterface + { + return new Browser(); + } +} diff --git a/tests/Model/TaskTest.php b/tests/Model/TaskTest.php new file mode 100644 index 00000000..6bb2b770 --- /dev/null +++ b/tests/Model/TaskTest.php @@ -0,0 +1,76 @@ +bugs = [ + new Bug(), + ]; + $this->revision = new Revision(); + $this->browser = new Browser(); + $this->task = $this->createTask(); + $this->task->setId(123); + $this->task->setTitle('bug title'); + $this->task->setModelRevision($this->revision); + $this->task->setAuthor(12); + $this->task->setRunning(true); + $this->task->setBrowser($this->browser); + $this->task->addBug($this->bugs[0]); + $this->task->setDebug(true); + } + + public function testProperties(): void + { + $this->assertSame(123, $this->task->getId()); + $this->assertSame('bug title', $this->task->getTitle()); + $this->assertSame($this->revision, $this->task->getModelRevision()); + $this->assertSame(12, $this->task->getAuthor()); + $this->assertSame(true, $this->task->isRunning()); + $this->assertSame($this->browser, $this->task->getBrowser()); + $this->assertSame($this->bugs, $this->task->getBugs()->toArray()); + $this->assertSame(true, $this->task->isDebug()); + $this->assertSame($this->task, $this->task->getTask()); + } + + public function testGetLogName(): void + { + $this->assertSame('task-123.log', $this->task->getLogName()); + } + + public function testGetVideoName(): void + { + $this->assertSame('task-123.mp4', $this->task->getVideoName()); + } + + protected function createTask(): TaskInterface + { + return new Task(); + } +} diff --git a/tests/Model/ValuesTest.php b/tests/Model/ValuesTest.php new file mode 100644 index 00000000..cda452bc --- /dev/null +++ b/tests/Model/ValuesTest.php @@ -0,0 +1,39 @@ + 'value', 'key2' => 'value2']); + $this->assertSame(['key' => 'value', 'key2' => 'value2'], $values->getValues()); + } + + public function testGetValue(): void + { + $values = new Values(['key' => 'value']); + $this->assertSame('value', $values->getValue('key')); + $this->assertNull($values->getValue('key2')); + } + + public function testSetValue(): void + { + $values = new Values(); + $this->assertSame([], $values->getValues()); + $values->setValue('key', 'value'); + $values->setValue('key2', null); + $values->setValue('key3', 123); + $values->setValue('key4', json_decode('{"test1":"Test 1","test2":{},"test3":{"test4":"Test 4"}}')); + $this->assertSame( + '{"key":"value","key2":null,"key3":123,"key4":{"test1":"Test 1","test2":{},"test3":{"test4":"Test 4"}}}', + json_encode($values->getValues()) + ); + } +} diff --git a/tests/Plugin/PluginManagerTest.php b/tests/Plugin/PluginManagerTest.php new file mode 100644 index 00000000..0803fa83 --- /dev/null +++ b/tests/Plugin/PluginManagerTest.php @@ -0,0 +1,86 @@ +plugin = $this->createPlugin(); + $this->locator = $this->createMock(ServiceLocator::class); + $this->pluginManager = $this->createPluginManager(); + } + + /** + * @dataProvider pluginProvider + */ + public function testGet(string $plugin): void + { + $this->locator->expects($this->once())->method('has')->with($plugin)->willReturn(true); + $this->locator->expects($this->once())->method('get')->with($plugin)->willReturn($this->plugin); + $this->assertSame($this->plugin, $this->pluginManager->{$this->getMethod}($plugin)); + } + + public function testAll(): void + { + $this->assertSame($this->plugins, $this->pluginManager->all()); + } + + /** + * @dataProvider pluginProvider + */ + public function testHas(string $plugin): void + { + $this->locator->expects($this->once())->method('has')->with($plugin)->willReturn(true); + $this->assertTrue($this->pluginManager->has($plugin)); + } + + public function testInvalidPlugin(): void + { + $plugin = 'invalid'; + $this->assertFalse(in_array($plugin, $this->plugins)); + $this->locator->expects($this->never())->method('has'); + $this->assertFalse($this->pluginManager->has($plugin)); + $this->expectExceptionObject(new UnexpectedValueException($this->getInvalidPluginExceptionMessage($plugin))); + $this->pluginManager->get($plugin); + } + + protected function createPluginManager(): PluginManagerInterface + { + return new PluginManager($this->locator, $this->plugins); + } + + protected function createPlugin(): PluginInterface + { + return $this->createMock(PluginInterface::class); + } + + protected function getInvalidPluginExceptionMessage(string $plugin): string + { + return sprintf('Plugin "%s" does not exist.', $plugin); + } + + public function pluginProvider(): array + { + return [ + $this->plugins, + ]; + } +} diff --git a/tests/Reducer/DispatcherTestCase.php b/tests/Reducer/DispatcherTestCase.php index 8ef5600a..9be6f841 100644 --- a/tests/Reducer/DispatcherTestCase.php +++ b/tests/Reducer/DispatcherTestCase.php @@ -3,6 +3,7 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Reducer; use PHPUnit\Framework\Constraint\Callback; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; @@ -12,10 +13,10 @@ use Tienvx\Bundle\MbtBundle\Model\BugInterface; use Tienvx\Bundle\MbtBundle\Reducer\DispatcherInterface; -class DispatcherTestCase extends TestCase +abstract class DispatcherTestCase extends TestCase { protected DispatcherInterface $dispatcher; - protected MessageBusInterface $messageBus; + protected MessageBusInterface|MockObject $messageBus; protected BugInterface $bug; protected array $pairs = []; @@ -25,46 +26,42 @@ protected function setUp(): void $this->bug = new Bug(); $this->bug->setId(123); $this->bug->setMessage('Something wrong'); - $this->bug->setSteps(array_map(fn () => $this->createMock(StepInterface::class), range(1, 11))); } - public function testDispatch(): void + /** + * @dataProvider stepsProvider + */ + public function testDispatch(int $length, array $expectedPairs): void { + if ($length > 0) { + $this->bug->setSteps(array_map(fn () => $this->createMock(StepInterface::class), range(0, $length - 1))); + } $this->messageBus - ->expects($this->exactly(4)) + ->expects($this->exactly(count($expectedPairs))) ->method('dispatch') - ->with($this->assertMessage()) + ->with($this->assertMessage($length)) ->willReturn(new Envelope(new \stdClass())); - $this->assertSame(4, $this->dispatcher->dispatch($this->bug)); - $this->assertPairs(); + $this->assertSame(count($expectedPairs), $this->dispatcher->dispatch($this->bug)); + $this->assertPairs($expectedPairs); } - protected function assertMessage(): Callback + protected function assertMessage(int $length): Callback { - return $this->callback(function ($message) { + return $this->callback(function ($message) use ($length) { if (!$message instanceof ReduceStepsMessage) { return false; } - if ( - $message->getBugId() !== $this->bug->getId() - || $message->getFrom() >= $message->getTo() - || 11 !== $message->getLength() - ) { - return false; - } $pair = [$message->getFrom(), $message->getTo()]; if (!in_array($pair, $this->pairs)) { $this->pairs[] = $pair; - - return true; } - return false; + return $message->getBugId() === $this->bug->getId() && + $length === $message->getLength(); }); } - protected function assertPairs(): void - { - $this->assertCount(4, $this->pairs); - } + abstract protected function assertPairs(array $expectedPairs): void; + + abstract public function stepsProvider(): array; } diff --git a/tests/Reducer/HandlerTestCase.php b/tests/Reducer/HandlerTestCase.php index ed7f3ed1..c73f7fe3 100644 --- a/tests/Reducer/HandlerTestCase.php +++ b/tests/Reducer/HandlerTestCase.php @@ -2,62 +2,52 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Reducer; -use Doctrine\DBAL\LockMode; -use Doctrine\ORM\EntityManagerInterface; use Exception; -use Facebook\WebDriver\Remote\DesiredCapabilities; -use Facebook\WebDriver\Remote\RemoteWebDriver; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub\Stub; +use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; +use Throwable; use Tienvx\Bundle\MbtBundle\Entity\Bug; +use Tienvx\Bundle\MbtBundle\Entity\Model\Revision; use Tienvx\Bundle\MbtBundle\Entity\Task; -use Tienvx\Bundle\MbtBundle\Exception\RuntimeException; +use Tienvx\Bundle\MbtBundle\Exception\StepsNotConnectedException; +use Tienvx\Bundle\MbtBundle\Message\CreateBugMessage; use Tienvx\Bundle\MbtBundle\Message\ReduceBugMessage; use Tienvx\Bundle\MbtBundle\Model\Bug\StepInterface; use Tienvx\Bundle\MbtBundle\Model\BugInterface; -use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; -use Tienvx\Bundle\MbtBundle\Model\TaskInterface; use Tienvx\Bundle\MbtBundle\Reducer\HandlerInterface; -use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelperInterface; -use Tienvx\Bundle\MbtBundle\Service\SelenoidHelperInterface; -use Tienvx\Bundle\MbtBundle\Service\StepRunnerInterface; -use Tienvx\Bundle\MbtBundle\Service\StepsBuilderInterface; -use Tienvx\Bundle\MbtBundle\Tests\StepsTestCase; +use Tienvx\Bundle\MbtBundle\Repository\BugRepositoryInterface; +use Tienvx\Bundle\MbtBundle\Service\ConfigInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Builder\StepsBuilderInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Runner\BugStepsRunner; -class HandlerTestCase extends StepsTestCase +abstract class HandlerTestCase extends TestCase { protected HandlerInterface $handler; - protected EntityManagerInterface $entityManager; - protected MessageBusInterface $messageBus; - protected StepRunnerInterface $stepRunner; - protected StepsBuilderInterface $stepsBuilder; - protected BugHelperInterface $bugHelper; - protected SelenoidHelperInterface $selenoidHelper; - protected DesiredCapabilities $capabilities; - protected RemoteWebDriver $driver; + protected BugRepositoryInterface|MockObject $bugRepository; + protected MessageBusInterface|MockObject $messageBus; + protected BugStepsRunner|MockObject $stepsRunner; + protected StepsBuilderInterface|MockObject $stepsBuilder; + protected ConfigInterface|MockObject $config; protected array $newSteps; + protected Revision $revision; protected BugInterface $bug; - protected TaskInterface $task; - protected RevisionInterface $revision; protected function setUp(): void { - $this->entityManager = $this->createMock(EntityManagerInterface::class); + $this->bugRepository = $this->createMock(BugRepositoryInterface::class); $this->messageBus = $this->createMock(MessageBusInterface::class); - $this->stepRunner = $this->createMock(StepRunnerInterface::class); + $this->stepsRunner = $this->createMock(BugStepsRunner::class); $this->stepsBuilder = $this->createMock(StepsBuilderInterface::class); - $this->bugHelper = $this->createMock(BugHelperInterface::class); - $this->selenoidHelper = $this->createMock(SelenoidHelperInterface::class); + $this->config = $this->createMock(ConfigInterface::class); $this->newSteps = [ $this->createMock(StepInterface::class), $this->createMock(StepInterface::class), $this->createMock(StepInterface::class), $this->createMock(StepInterface::class), ]; - $this->revision = $this->createMock(RevisionInterface::class); - $this->task = new Task(); - $this->task->setId(123); - $this->task->setModelRevision($this->revision); $this->bug = new Bug(); $this->bug->setId(1); $this->bug->setMessage('Something wrong'); @@ -68,116 +58,114 @@ protected function setUp(): void $this->createMock(StepInterface::class), $this->createMock(StepInterface::class), ]); - $this->task->addBug($this->bug); + $this->bug->setDebug(true); + $this->revision = new Revision(); + $task = new Task(); + $task->setId(123); + $task->setModelRevision($this->revision); + $this->bug->setTask($task); $this->stepsBuilder ->expects($this->once()) ->method('create') ->with($this->bug, 1, 2) ->willReturn((fn () => yield from $this->newSteps)()); - $this->driver = $this->createMock(RemoteWebDriver::class); - $this->capabilities = new DesiredCapabilities(); + } + + public function testHandleNotConnectedSteps(): void + { + $this->expectStepsBuilder($this->throwException(new StepsNotConnectedException())); + $this->stepsRunner->expects($this->never())->method('run'); + $this->handler->handle($this->bug, 1, 2); } public function testHandleOldBug(): void { - $this->driver->expects($this->never())->method('quit'); - $this->selenoidHelper->expects($this->never())->method('getCapabilities'); - $this->selenoidHelper->expects($this->never())->method('createDriver'); $this->bug->setSteps([ $this->createMock(StepInterface::class), $this->createMock(StepInterface::class), $this->createMock(StepInterface::class), ]); - $this->stepRunner->expects($this->never())->method('run'); + $this->expectStepsBuilder($this->returnValue((fn () => yield from $this->newSteps)())); + $this->stepsRunner->expects($this->never())->method('run'); $this->handler->handle($this->bug, 1, 2); } - public function testRun(): void + /** + * @dataProvider exceptionProvider + */ + public function testHandle(?Throwable $exception, bool $updateSteps, bool $createNewBug): void { - $this->driver->expects($this->once())->method('quit'); - $this->selenoidHelper - ->expects($this->once()) - ->method('getCapabilities') - ->with($this->bug) - ->willReturn($this->capabilities); - $this->selenoidHelper - ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->stepRunner->expects($this->exactly(4)) - ->method('run')->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver); + $this->expectStepsBuilder($this->returnValue((fn () => yield from $this->newSteps)())); + $this->stepsRunner->expects($this->once()) + ->method('run') + ->with( + $this->newSteps, + $this->bug, + $this->callback(function (callable $exceptionCallback) use ($exception) { + if ($exception) { + $exceptionCallback($exception); + } + + return true; + }) + ); + if ($updateSteps) { + $this->bugRepository + ->expects($this->once()) + ->method('updateSteps') + ->with($this->bug, $this->newSteps); + $this->messageBus + ->expects($this->once()) + ->method('dispatch') + ->with($this->callback(function ($message) { + return $message instanceof ReduceBugMessage && $message->getId() === $this->bug->getId(); + })) + ->willReturn(new Envelope(new \stdClass())); + } else { + $this->bugRepository->expects($this->never())->method('updateSteps'); + if ($exception) { + $this->config + ->expects($this->once()) + ->method('shouldCreateNewBugWhileReducing') + ->willReturn($createNewBug); + if ($createNewBug) { + $this->messageBus + ->expects($this->once()) + ->method('dispatch') + ->with($this->callback(function ($message) use ($exception) { + return $message instanceof CreateBugMessage && + $message->getMessage() === $exception->getMessage() && + $message->getSteps() === $this->newSteps && + 123 === $message->getTaskId(); + })) + ->willReturn(new Envelope(new \stdClass())); + } else { + $this->messageBus->expects($this->never())->method('dispatch'); + } + } else { + $this->messageBus->expects($this->never())->method('dispatch'); + } + } $this->handler->handle($this->bug, 1, 2); + $this->assertFalse($this->bug->isDebug()); } - public function testRunIntoException(): void + public function exceptionProvider(): array { - $this->driver->expects($this->once())->method('quit'); - $this->selenoidHelper - ->expects($this->once()) - ->method('getCapabilities') - ->with($this->bug) - ->willReturn($this->capabilities); - $this->selenoidHelper - ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->stepRunner->expects($this->exactly(4))->method('run') - ->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver) - ->will($this->onConsecutiveCalls( - null, - null, - null, - $this->throwException(new RuntimeException('Something else wrong')), - )); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Something else wrong'); - $this->handler->handle($this->bug, 1, 2); + return [ + [null, false, false], + [new Exception('Something else wrong'), false, false], + [new Exception('Something else wrong'), false, true], + [new Exception('Something wrong'), true, false], + ]; } - public function testRunFoundSameBug(): void + protected function expectStepsBuilder(Stub $will): void { - $this->driver->expects($this->once())->method('quit'); - $this->selenoidHelper - ->expects($this->once()) - ->method('getCapabilities') - ->with($this->bug) - ->willReturn($this->capabilities); - $this->selenoidHelper - ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->entityManager->expects($this->once())->method('refresh')->with($this->bug); - $this->entityManager - ->expects($this->once()) - ->method('lock') - ->with($this->bug, LockMode::PESSIMISTIC_WRITE); - $this->entityManager - ->expects($this->once()) - ->method('transactional') - ->with($this->callback(function ($callback) { - $callback(); - - return true; - })); - $this->messageBus + $this->stepsBuilder ->expects($this->once()) - ->method('dispatch') - ->with($this->isInstanceOf(ReduceBugMessage::class)) - ->willReturn(new Envelope(new \stdClass())); - $this->stepRunner->expects($this->exactly(4))->method('run') - ->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver) - ->will($this->onConsecutiveCalls( - null, - null, - null, - $this->throwException(new Exception('Something wrong')), - )); - $this->handler->handle($this->bug, 1, 2); - $this->assertSteps($this->newSteps, $this->bug->getSteps()); - $this->assertSame(0, $this->bug->getProgress()->getProcessed()); - $this->assertSame(0, $this->bug->getProgress()->getTotal()); + ->method('create') + ->with($this->bug, 1, 2) + ->will($will); } } diff --git a/tests/Reducer/Random/RandomDispatcherTest.php b/tests/Reducer/Random/RandomDispatcherTest.php index d132a36a..d8e7cb48 100644 --- a/tests/Reducer/Random/RandomDispatcherTest.php +++ b/tests/Reducer/Random/RandomDispatcherTest.php @@ -21,4 +21,28 @@ protected function setUp(): void parent::setUp(); $this->dispatcher = new RandomDispatcher($this->messageBus); } + + public function stepsProvider(): array + { + return [ + [0, []], + [1, []], + [2, []], + [3, range(1, 2)], + [4, range(1, 2)], + [5, range(1, 3)], + [6, range(1, 3)], + [9, range(1, 3)], + [11, range(1, 4)], + ]; + } + + protected function assertPairs(array $expectedPairs): void + { + if (count($expectedPairs) > 1) { + $this->assertCount(count($expectedPairs), $this->pairs); + } else { + $this->assertSame($expectedPairs, $this->pairs); + } + } } diff --git a/tests/Reducer/Random/RandomHandlerTest.php b/tests/Reducer/Random/RandomHandlerTest.php index ae304bfc..727ff67c 100644 --- a/tests/Reducer/Random/RandomHandlerTest.php +++ b/tests/Reducer/Random/RandomHandlerTest.php @@ -15,7 +15,9 @@ * @uses \Tienvx\Bundle\MbtBundle\Model\Task * @uses \Tienvx\Bundle\MbtBundle\Model\Bug\Step * @uses \Tienvx\Bundle\MbtBundle\Message\ReduceBugMessage + * @uses \Tienvx\Bundle\MbtBundle\Message\CreateBugMessage * @uses \Tienvx\Bundle\MbtBundle\Model\Progress + * @uses \Tienvx\Bundle\MbtBundle\Model\Debug */ class RandomHandlerTest extends HandlerTestCase { @@ -23,12 +25,11 @@ protected function setUp(): void { parent::setUp(); $this->handler = new RandomHandler( - $this->entityManager, + $this->bugRepository, $this->messageBus, - $this->stepRunner, + $this->stepsRunner, $this->stepsBuilder, - $this->bugHelper, - $this->selenoidHelper + $this->config ); } } diff --git a/tests/Reducer/Random/RandomReducerTest.php b/tests/Reducer/Random/RandomReducerTest.php index 165ddda5..3b4b2004 100644 --- a/tests/Reducer/Random/RandomReducerTest.php +++ b/tests/Reducer/Random/RandomReducerTest.php @@ -2,6 +2,7 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Reducer\Random; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Tienvx\Bundle\MbtBundle\Entity\Bug; use Tienvx\Bundle\MbtBundle\Reducer\Random\RandomDispatcher; @@ -18,8 +19,8 @@ */ class RandomReducerTest extends TestCase { - protected RandomDispatcher $dispatcher; - protected RandomHandler $handler; + protected RandomDispatcher|MockObject $dispatcher; + protected RandomHandler|MockObject $handler; protected RandomReducer $reducer; protected function setUp(): void @@ -39,6 +40,11 @@ public function testGetName(): void $this->assertSame('random', RandomReducer::getName()); } + public function testIsSupported(): void + { + $this->assertTrue(RandomReducer::isSupported()); + } + public function testDispatch(): void { $bug = new Bug(); diff --git a/tests/Reducer/ReducerManagerTest.php b/tests/Reducer/ReducerManagerTest.php index 54228dcb..3936ebeb 100644 --- a/tests/Reducer/ReducerManagerTest.php +++ b/tests/Reducer/ReducerManagerTest.php @@ -2,60 +2,34 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Reducer; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ServiceLocator; -use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; +use Tienvx\Bundle\MbtBundle\Plugin\PluginInterface; +use Tienvx\Bundle\MbtBundle\Plugin\PluginManagerInterface; use Tienvx\Bundle\MbtBundle\Reducer\ReducerInterface; use Tienvx\Bundle\MbtBundle\Reducer\ReducerManager; +use Tienvx\Bundle\MbtBundle\Tests\Plugin\PluginManagerTest; /** * @covers \Tienvx\Bundle\MbtBundle\Reducer\ReducerManager - * @covers \Tienvx\Bundle\MbtBundle\Plugin\AbstractPluginManager + * + * @uses \Tienvx\Bundle\MbtBundle\Plugin\PluginManager */ -class ReducerManagerTest extends TestCase +class ReducerManagerTest extends PluginManagerTest { - protected ReducerManager $reducerManager; - protected ReducerInterface $reducer; - protected ServiceLocator $locator; + protected array $plugins = ['split', 'random']; + protected string $getMethod = 'getReducer'; - protected function setUp(): void + protected function createPluginManager(): PluginManagerInterface { - $this->reducer = $this->createMock(ReducerInterface::class); - $this->locator = $this->createMock(ServiceLocator::class); - $plugins = ['split', 'random']; - $this->reducerManager = new ReducerManager($this->locator, $plugins); + return new ReducerManager($this->locator, $this->plugins); } - public function testGet(): void + protected function createPlugin(): PluginInterface { - $this->locator->expects($this->once())->method('has')->with('random')->willReturn(true); - $this->locator->expects($this->once())->method('get')->with('random')->willReturn($this->reducer); - $this->assertSame($this->reducer, $this->reducerManager->get('random')); + return $this->createMock(ReducerInterface::class); } - public function testAll(): void + protected function getInvalidPluginExceptionMessage(string $plugin): string { - $this->assertSame(['split', 'random'], $this->reducerManager->all()); - } - - public function testHasSplit(): void - { - $this->locator->expects($this->once())->method('has')->with('split')->willReturn(true); - $this->assertTrue($this->reducerManager->has('split')); - } - - public function testHasRandom(): void - { - $this->locator->expects($this->once())->method('has')->with('random')->willReturn(true); - $this->assertTrue($this->reducerManager->has('random')); - } - - public function testDoesNotHaveOther(): void - { - $this->locator->expects($this->never())->method('has'); - $this->assertFalse($this->reducerManager->has('other')); - $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('Reducer "other" does not exist.'); - $this->reducerManager->getReducer('other'); + return sprintf('Reducer "%s" does not exist.', $plugin); } } diff --git a/tests/Reducer/Split/SplitDispatcherTest.php b/tests/Reducer/Split/SplitDispatcherTest.php index 852ad0d1..daa0a1fd 100644 --- a/tests/Reducer/Split/SplitDispatcherTest.php +++ b/tests/Reducer/Split/SplitDispatcherTest.php @@ -22,14 +22,64 @@ protected function setUp(): void $this->dispatcher = new SplitDispatcher($this->messageBus); } - protected function assertPairs(): void + public function stepsProvider(): array { - parent::assertPairs(); - $this->assertSame([ - [0, 3], - [3, 6], - [6, 9], - [9, 10], - ], $this->pairs); + return [ + [0, []], + [1, []], + [2, []], + [3, [ + [0, 2], + ]], + [4, [ + [0, 2], + [2, 3], + ]], + [5, [ + [0, 2], + [2, 4], + ]], + [6, [ + [0, 2], + [2, 4], + [4, 5], + ]], + [7, [ + [0, 3], + [3, 6], + ]], + [8, [ + [0, 3], + [3, 6], + [6, 7], + ]], + [9, [ + [0, 3], + [3, 6], + [6, 8], + ]], + [10, [ + [0, 3], + [3, 6], + [6, 9], + ]], + [11, [ + [0, 3], + [3, 6], + [6, 9], + [9, 10], + ]], + [12, [ + [0, 3], + [3, 6], + [6, 9], + [9, 11], + ]], + ]; + } + + protected function assertPairs(array $expectedPairs): void + { + $this->assertSame($expectedPairs, $this->pairs); } } diff --git a/tests/Reducer/Split/SplitHandlerTest.php b/tests/Reducer/Split/SplitHandlerTest.php index 4d108448..edbe06f7 100644 --- a/tests/Reducer/Split/SplitHandlerTest.php +++ b/tests/Reducer/Split/SplitHandlerTest.php @@ -15,7 +15,9 @@ * @uses \Tienvx\Bundle\MbtBundle\Model\Task * @uses \Tienvx\Bundle\MbtBundle\Model\Bug\Step * @uses \Tienvx\Bundle\MbtBundle\Message\ReduceBugMessage + * @uses \Tienvx\Bundle\MbtBundle\Message\CreateBugMessage * @uses \Tienvx\Bundle\MbtBundle\Model\Progress + * @uses \Tienvx\Bundle\MbtBundle\Model\Debug */ class SplitHandlerTest extends HandlerTestCase { @@ -23,12 +25,11 @@ protected function setUp(): void { parent::setUp(); $this->handler = new SplitHandler( - $this->entityManager, + $this->bugRepository, $this->messageBus, - $this->stepRunner, + $this->stepsRunner, $this->stepsBuilder, - $this->bugHelper, - $this->selenoidHelper + $this->config ); } } diff --git a/tests/Reducer/Split/SplitReducerTest.php b/tests/Reducer/Split/SplitReducerTest.php index 400a55e2..a5c92992 100644 --- a/tests/Reducer/Split/SplitReducerTest.php +++ b/tests/Reducer/Split/SplitReducerTest.php @@ -2,6 +2,7 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Reducer\Split; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Tienvx\Bundle\MbtBundle\Entity\Bug; use Tienvx\Bundle\MbtBundle\Reducer\ReducerManager; @@ -18,8 +19,8 @@ */ class SplitReducerTest extends TestCase { - protected SplitDispatcher $dispatcher; - protected SplitHandler $handler; + protected SplitDispatcher|MockObject $dispatcher; + protected SplitHandler|MockObject $handler; protected SplitReducer $reducer; protected function setUp(): void @@ -39,6 +40,11 @@ public function testGetName(): void $this->assertSame('split', SplitReducer::getName()); } + public function testIsSupported(): void + { + $this->assertTrue(SplitReducer::isSupported()); + } + public function testDispatch(): void { $bug = new Bug(); diff --git a/tests/Repository/BugRepositoryTest.php b/tests/Repository/BugRepositoryTest.php new file mode 100644 index 00000000..217e095e --- /dev/null +++ b/tests/Repository/BugRepositoryTest.php @@ -0,0 +1,165 @@ +manager = $this->createMock(EntityManagerDecorator::class); + $this->manager + ->expects($this->once()) + ->method('getClassMetadata') + ->with(Bug::class) + ->willReturn($this->createMock(ClassMetadata::class)); + $managerRegistry = $this->createMock(ManagerRegistry::class); + $managerRegistry + ->expects($this->once()) + ->method('getManagerForClass') + ->with(Bug::class) + ->willReturn($this->manager); + $this->bugRepository = new BugRepository($managerRegistry); + $progress = new Progress(); + $progress->setTotal(10); + $progress->setProcessed(5); + $this->bug = new Bug(); + $this->bug->setProgress($progress); + $this->bug->setSteps([ + $this->createMock(StepInterface::class), + $this->createMock(StepInterface::class), + $this->createMock(StepInterface::class), + ]); + $this->connection = $this->createMock(Connection::class); + } + + /** + * @dataProvider stepsProvider + */ + public function testUpdateSteps(int $length, int $expectedLength, int $expectedProcessed, int $expectedTotal): void + { + $newSteps = array_map(fn () => $this->createMock(StepInterface::class), range(0, $length - 1)); + $this->manager->expects($this->once())->method('refresh')->with($this->bug); + $this->manager + ->expects($this->exactly(count($this->bug->getSteps()) !== $expectedLength)) + ->method('lock') + ->with($this->bug, LockMode::PESSIMISTIC_WRITE); + $this->manager + ->expects($this->once()) + ->method('wrapInTransaction') + ->with($this->callback(function ($callback) { + $callback(); + + return true; + })); + $this->bugRepository->updateSteps($this->bug, $newSteps); + $this->assertCount($expectedLength, $this->bug->getSteps()); + $this->assertSame($expectedProcessed, $this->bug->getProgress()->getProcessed()); + $this->assertSame($expectedTotal, $this->bug->getProgress()->getTotal()); + } + + public function stepsProvider(): array + { + return [ + [4, 3, 5, 10], + [3, 3, 5, 10], + [2, 2, 0, 0], + ]; + } + + public function testIncreaseProcessed(): void + { + $this->manager->expects($this->once())->method('refresh')->with($this->bug); + $this->manager->expects($this->once())->method('lock')->with($this->bug, LockMode::PESSIMISTIC_WRITE); + $this->manager + ->expects($this->once()) + ->method('wrapInTransaction') + ->with($this->callback(function ($callback) { + $callback(); + + return true; + })); + $this->bugRepository->increaseProcessed($this->bug, 2); + $this->assertSame(7, $this->bug->getProgress()->getProcessed()); + $this->assertSame(10, $this->bug->getProgress()->getTotal()); + } + + public function testIncreaseProcessedReachLimit(): void + { + $this->manager->expects($this->once())->method('refresh')->with($this->bug); + $this->manager->expects($this->once())->method('lock')->with($this->bug, LockMode::PESSIMISTIC_WRITE); + $this->manager + ->expects($this->once()) + ->method('wrapInTransaction') + ->with($this->callback(function ($callback) { + $callback(); + + return true; + })); + $this->bugRepository->increaseProcessed($this->bug, 6); + $this->assertSame(10, $this->bug->getProgress()->getProcessed()); + $this->assertSame(10, $this->bug->getProgress()->getTotal()); + } + + public function testIncreaseTotal(): void + { + $this->manager->expects($this->once())->method('refresh')->with($this->bug); + $this->manager->expects($this->once())->method('lock')->with($this->bug, LockMode::PESSIMISTIC_WRITE); + $this->manager + ->expects($this->once()) + ->method('wrapInTransaction') + ->with($this->callback(function ($callback) { + $callback(); + + return true; + })); + $this->bugRepository->increaseTotal($this->bug, 3); + $this->assertSame(5, $this->bug->getProgress()->getProcessed()); + $this->assertSame(13, $this->bug->getProgress()->getTotal()); + } + + public function testStartRecordingBug(): void + { + $this->bug->getVideo()->setRecording(false); + $this->manager->expects($this->once())->method('refresh')->with($this->bug); + $this->manager->expects($this->once())->method('flush'); + $this->bugRepository->startRecording($this->bug); + $this->assertTrue($this->bug->getVideo()->isRecording()); + } + + public function testStopRecordingBug(): void + { + $this->connection->expects($this->once())->method('connect'); + $this->bug->getVideo()->setRecording(true); + $this->manager->expects($this->once())->method('flush'); + $this->manager->expects($this->once())->method('getConnection')->willReturn($this->connection); + $this->bugRepository->stopRecording($this->bug); + $this->assertFalse($this->bug->getVideo()->isRecording()); + } +} diff --git a/tests/Repository/TaskRepositoryTest.php b/tests/Repository/TaskRepositoryTest.php new file mode 100644 index 00000000..82b8bcc2 --- /dev/null +++ b/tests/Repository/TaskRepositoryTest.php @@ -0,0 +1,76 @@ +manager = $this->createMock(EntityManagerDecorator::class); + $this->manager + ->expects($this->once()) + ->method('getClassMetadata') + ->with(Task::class) + ->willReturn($this->createMock(ClassMetadata::class)); + $managerRegistry = $this->createMock(ManagerRegistry::class); + $managerRegistry + ->expects($this->once()) + ->method('getManagerForClass') + ->with(Task::class) + ->willReturn($this->manager); + $this->taskRepository = new TaskRepository($managerRegistry); + $this->task = new Task(); + } + + public function testStartRunningTask(): void + { + $this->task->setRunning(false); + $this->manager->expects($this->once())->method('flush'); + $this->taskRepository->startRunning($this->task); + $this->assertTrue($this->task->isRunning()); + } + + public function testStopRunningTask(): void + { + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('connect'); + $this->task->setRunning(true); + $this->manager->expects($this->once())->method('flush'); + $this->manager->expects($this->once())->method('getConnection')->willReturn($connection); + $this->taskRepository->stopRunning($this->task); + $this->assertFalse($this->task->isRunning()); + } + + public function testAddBug(): void + { + $bug = new Bug(); + $this->assertEmpty($this->task->getBugs()); + $this->manager->expects($this->once())->method('flush'); + $this->taskRepository->addBug($this->task, $bug); + $this->assertSame([$bug], $this->task->getBugs()->toArray()); + } +} diff --git a/tests/Service/Bug/BugHelperTest.php b/tests/Service/Bug/BugHelperTest.php index 4b73ed7a..40af8967 100644 --- a/tests/Service/Bug/BugHelperTest.php +++ b/tests/Service/Bug/BugHelperTest.php @@ -2,36 +2,31 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Service\Bug; -use Doctrine\DBAL\Connection; -use Doctrine\ORM\EntityManagerInterface; use Exception; -use Facebook\WebDriver\Remote\DesiredCapabilities; -use Facebook\WebDriver\Remote\RemoteWebDriver; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException; use Symfony\Component\Messenger\MessageBusInterface; +use Throwable; use Tienvx\Bundle\MbtBundle\Entity\Bug; use Tienvx\Bundle\MbtBundle\Entity\Model\Revision; use Tienvx\Bundle\MbtBundle\Entity\Progress; use Tienvx\Bundle\MbtBundle\Entity\Task; -use Tienvx\Bundle\MbtBundle\Exception\RuntimeException; use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; use Tienvx\Bundle\MbtBundle\Message\RecordVideoMessage; use Tienvx\Bundle\MbtBundle\Message\ReportBugMessage; use Tienvx\Bundle\MbtBundle\Model\Bug\StepInterface; use Tienvx\Bundle\MbtBundle\Model\BugInterface; -use Tienvx\Bundle\MbtBundle\Model\Model\RevisionInterface; use Tienvx\Bundle\MbtBundle\Model\ProgressInterface; -use Tienvx\Bundle\MbtBundle\Model\TaskInterface; use Tienvx\Bundle\MbtBundle\Reducer\ReducerInterface; use Tienvx\Bundle\MbtBundle\Reducer\ReducerManagerInterface; +use Tienvx\Bundle\MbtBundle\Repository\BugRepositoryInterface; use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelper; use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelperInterface; use Tienvx\Bundle\MbtBundle\Service\Bug\BugNotifierInterface; -use Tienvx\Bundle\MbtBundle\Service\Bug\BugProgressInterface; use Tienvx\Bundle\MbtBundle\Service\ConfigInterface; -use Tienvx\Bundle\MbtBundle\Service\SelenoidHelperInterface; -use Tienvx\Bundle\MbtBundle\Service\StepRunnerInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Runner\BugStepsRunner; /** * @covers \Tienvx\Bundle\MbtBundle\Service\Bug\BugHelper @@ -42,87 +37,64 @@ * @uses \Tienvx\Bundle\MbtBundle\Model\Bug * @uses \Tienvx\Bundle\MbtBundle\Model\Task * @uses \Tienvx\Bundle\MbtBundle\Model\Progress + * @uses \Tienvx\Bundle\MbtBundle\Model\Bug\Video * @uses \Tienvx\Bundle\MbtBundle\Message\ReportBugMessage * @uses \Tienvx\Bundle\MbtBundle\Message\RecordVideoMessage + * @uses \Tienvx\Bundle\MbtBundle\Model\Debug */ class BugHelperTest extends TestCase { - protected ReducerManagerInterface $reducerManager; - protected EntityManagerInterface $entityManager; - protected MessageBusInterface $messageBus; - protected BugProgressInterface $bugProgress; - protected BugNotifierInterface $notifyHelper; - protected StepRunnerInterface $stepRunner; - protected SelenoidHelperInterface $selenoidHelper; - protected ConfigInterface $config; + protected ReducerManagerInterface|MockObject $reducerManager; + protected BugRepositoryInterface|MockObject $bugRepository; + protected MessageBusInterface|MockObject $messageBus; + protected BugNotifierInterface|MockObject $bugNotifier; + protected BugStepsRunner|MockObject $stepsRunner; + protected ConfigInterface|MockObject $config; protected BugHelperInterface $helper; - protected Connection $connection; - protected TaskInterface $task; + protected Revision $revision; protected BugInterface $bug; protected ProgressInterface $progress; - protected RevisionInterface $revision; - protected DesiredCapabilities $capabilities; - protected RemoteWebDriver $driver; protected function setUp(): void { $this->reducerManager = $this->createMock(ReducerManagerInterface::class); - $this->entityManager = $this->createMock(EntityManagerInterface::class); + $this->bugRepository = $this->createMock(BugRepositoryInterface::class); $this->messageBus = $this->createMock(MessageBusInterface::class); - $this->bugProgress = $this->createMock(BugProgressInterface::class); - $this->notifyHelper = $this->createMock(BugNotifierInterface::class); - $this->stepRunner = $this->createMock(StepRunnerInterface::class); - $this->selenoidHelper = $this->createMock(SelenoidHelperInterface::class); + $this->bugNotifier = $this->createMock(BugNotifierInterface::class); + $this->stepsRunner = $this->createMock(BugStepsRunner::class); $this->config = $this->createMock(ConfigInterface::class); $this->helper = new BugHelper( $this->reducerManager, - $this->entityManager, + $this->bugRepository, $this->messageBus, - $this->bugProgress, - $this->notifyHelper, - $this->stepRunner, - $this->selenoidHelper, + $this->bugNotifier, + $this->stepsRunner, $this->config ); - $this->connection = $this->createMock(Connection::class); - $this->revision = new Revision(); - $this->task = new Task(); - $this->task->setModelRevision($this->revision); $this->progress = new Progress(); $this->progress->setTotal(10); $this->progress->setProcessed(9); $this->bug = new Bug(); $this->bug->setProgress($this->progress); + $this->bug->setMessage('Something wrong'); $this->bug->setId(123); - $this->bug->setTask($this->task); $this->bug->setSteps([ $this->createMock(StepInterface::class), $this->createMock(StepInterface::class), $this->createMock(StepInterface::class), ]); - $this->driver = $this->createMock(RemoteWebDriver::class); - $this->capabilities = new DesiredCapabilities(); - } - - public function testCreateBug(): void - { - $steps = [ - $this->createMock(StepInterface::class), - $this->createMock(StepInterface::class), - $this->createMock(StepInterface::class), - ]; - $bug = $this->helper->createBug($steps, 'Something wrong'); - $this->assertInstanceOf(BugInterface::class, $bug); - $this->assertSame($steps, $bug->getSteps()); - $this->assertSame('', $bug->getTitle()); - $this->assertSame('Something wrong', $bug->getMessage()); + $this->bug->setDebug(false); + $this->revision = new Revision(); + $task = new Task(); + $task->setModelRevision($this->revision); + $this->bug->setTask($task); } public function testReduceMissingBug(): void { $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Can not reduce bug 123: bug not found'); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn(null); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn(null); $this->helper->reduceBug(123); } @@ -133,8 +105,8 @@ public function testReduceBugDispatchMessages(): void $this->config->expects($this->once())->method('getReducer')->willReturn('random'); $this->reducerManager->expects($this->once())->method('getReducer')->with('random')->willReturn($reducer); $this->messageBus->expects($this->never())->method('dispatch'); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); - $this->bugProgress->expects($this->once())->method('increaseTotal')->with($this->bug, 5); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn($this->bug); + $this->bugRepository->expects($this->once())->method('increaseTotal')->with($this->bug, 5); $this->helper->reduceBug(123); } @@ -145,12 +117,12 @@ public function testReduceBugNotDispatchMessages(): void $this->config->expects($this->once())->method('getReducer')->willReturn('random'); $this->reducerManager->expects($this->once())->method('getReducer')->with('random')->willReturn($reducer); $this->messageBus->expects($this->never())->method('dispatch'); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); - $this->bugProgress->expects($this->never())->method('increaseTotal'); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn($this->bug); + $this->bugRepository->expects($this->never())->method('increaseTotal'); $this->helper->reduceBug(123); } - public function testFinishReduceBug(): void + public function testReduceBugRecordAndReport(): void { $this->progress->setProcessed(10); $reducer = $this->createMock(ReducerInterface::class); @@ -163,14 +135,14 @@ public function testFinishReduceBug(): void ->method('dispatch') ->with($this->callback(function ($message) { return ( - $message instanceof RecordVideoMessage - || $message instanceof ReportBugMessage - ) + $message instanceof RecordVideoMessage + || $message instanceof ReportBugMessage + ) && 123 === $message->getBugId(); })) ->willReturn(new Envelope(new \stdClass())); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); - $this->bugProgress->expects($this->never())->method('increaseTotal'); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn($this->bug); + $this->bugRepository->expects($this->never())->method('increaseTotal'); $this->helper->reduceBug(123); } @@ -178,24 +150,17 @@ public function testReportMissingBug(): void { $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Can not report bug 123: bug not found'); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn(null); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn(null); $this->helper->reportBug(123); } public function testReportBug(): void { - $task = new Task(); - $task->setAuthor(22); - $bug = new Bug(); - $bug->setTitle('New bug found'); - $bug->setId(123); - $bug->setMessage('Something wrong'); - $bug->setTask($task); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($bug); - $this->notifyHelper + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn($this->bug); + $this->bugNotifier ->expects($this->once()) ->method('notify') - ->with($bug); + ->with($this->bug); $this->helper->reportBug(123); } @@ -203,18 +168,21 @@ public function testReduceStepsMissingBug(): void { $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Can not reduce steps for bug 123: bug not found'); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn(null); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn(null); $this->helper->reduceSteps(123, 6, 1, 2); } public function testReduceReducedSteps(): void { $this->reducerManager->expects($this->never())->method('getReducer'); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn($this->bug); $this->helper->reduceSteps(123, 4, 1, 2); } - public function testNotStopReduceSteps(): void + /** + * @dataProvider messageProvider + */ + public function testReduceSteps(int $processed, bool $reportBug, array $messages): void { $this->bug->setSteps([ $this->createMock(StepInterface::class), @@ -226,153 +194,87 @@ public function testNotStopReduceSteps(): void $reducer->expects($this->once())->method('handle')->with($this->bug, 1, 2); $this->config->expects($this->once())->method('getReducer')->willReturn('random'); $this->reducerManager->expects($this->once())->method('getReducer')->with('random')->willReturn($reducer); - $this->messageBus->expects($this->never())->method('dispatch'); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); - $this->bugProgress->expects($this->once())->method('increaseProcessed')->with($this->bug, 1); - $this->helper->reduceSteps(123, 4, 1, 2); - } - - public function testStopReduceStepsAndRecordVideo(): void - { - $this->bug->setSteps([ - $this->createMock(StepInterface::class), - $this->createMock(StepInterface::class), - $this->createMock(StepInterface::class), - $this->createMock(StepInterface::class), - ]); - $reducer = $this->createMock(ReducerInterface::class); - $reducer->expects($this->once())->method('handle')->with($this->bug, 1, 2); - $this->config->expects($this->once())->method('getReducer')->willReturn('random'); - $this->reducerManager->expects($this->once())->method('getReducer')->with('random')->willReturn($reducer); - $this->messageBus - ->expects($this->exactly(1)) - ->method('dispatch') - ->with($this->callback( - fn ($message) => $message instanceof RecordVideoMessage && 123 === $message->getBugId() - )) - ->willReturn(new Envelope(new \stdClass())); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); - $this->bugProgress + if ($processed < $this->progress->getTotal()) { + $this->messageBus->expects($this->never())->method('dispatch'); + $this->config->expects($this->never())->method('shouldReportBug'); + } else { + $this->messageBus + ->expects($this->exactly(count($messages))) + ->method('dispatch') + ->withConsecutive(...array_map(fn (string $className) => [$this->callback( + fn ($message) => is_a($message, $className) && 123 === $message->getBugId() + )], $messages)) + ->willReturn(new Envelope(new \stdClass())); + $this->config->expects($this->once())->method('shouldReportBug')->willReturn($reportBug); + } + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn($this->bug); + $this->bugRepository ->expects($this->once()) ->method('increaseProcessed') ->with($this->bug, 1) - ->willReturnCallback(fn () => $this->progress->setProcessed(10)); + ->willReturnCallback(fn () => $this->progress->setProcessed($processed)); $this->helper->reduceSteps(123, 4, 1, 2); } - public function testStopReduceStepsAndReportBug(): void + public function messageProvider(): array { - $this->bug->setSteps([ - $this->createMock(StepInterface::class), - $this->createMock(StepInterface::class), - $this->createMock(StepInterface::class), - $this->createMock(StepInterface::class), - ]); - $reducer = $this->createMock(ReducerInterface::class); - $reducer->expects($this->once())->method('handle')->with($this->bug, 1, 2); - $this->config->expects($this->once())->method('getReducer')->willReturn('random'); - $this->reducerManager->expects($this->once())->method('getReducer')->with('random')->willReturn($reducer); - $this->config->expects($this->once())->method('shouldReportBug')->willReturn(true); - $this->messageBus - ->expects($this->exactly(2)) - ->method('dispatch') - ->withConsecutive( - [ - $this->callback( - fn ($message) => $message instanceof RecordVideoMessage && 123 === $message->getBugId() - ), - ], - [ - $this->callback( - fn ($message) => $message instanceof ReportBugMessage && 123 === $message->getBugId() - ), - ] - ) - ->willReturn(new Envelope(new \stdClass())); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); - $this->bugProgress - ->expects($this->once()) - ->method('increaseProcessed') - ->with($this->bug, 1) - ->willReturnCallback(fn () => $this->progress->setProcessed(10)); - $this->helper->reduceSteps(123, 4, 1, 2); + return [ + [9, true, []], + [10, false, [RecordVideoMessage::class]], + [10, true, [RecordVideoMessage::class, ReportBugMessage::class]], + ]; } public function testRecordVideoMissingBug(): void { $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Can not record video for bug 123: bug not found'); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn(null); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn(null); $this->helper->recordVideo(123); } - public function testRecordVideoThrowException(): void + public function testRecordVideoAlreadyRecording(): void { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Exception that we care about'); - $this->driver->expects($this->once())->method('quit'); - $this->selenoidHelper - ->expects($this->once()) - ->method('getCapabilities') - ->with($this->bug, true) - ->willReturn($this->capabilities); - $this->selenoidHelper - ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->stepRunner - ->expects($this->once()) - ->method('run') - ->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver) - ->willThrowException(new RuntimeException('Exception that we care about')); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); + $this->expectException(RecoverableMessageHandlingException::class); + $this->expectExceptionMessage('Can not record video for bug 123: bug is recording. Will retry later'); + $this->bug->getVideo()->setRecording(true); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn($this->bug); $this->helper->recordVideo(123); } - public function testRecordVideoNotThrowException(): void + /** + * @dataProvider exceptionProvider + */ + public function testRecordVideo(?Throwable $exception, ?string $expectedVideoErrorMessage): void { - $this->driver->expects($this->once())->method('quit'); - $this->selenoidHelper - ->expects($this->once()) - ->method('getCapabilities') - ->with($this->bug, true) - ->willReturn($this->capabilities); - $this->selenoidHelper + $this->stepsRunner ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->stepRunner - ->expects($this->exactly(2)) ->method('run') - ->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver) - ->willReturnOnConsecutiveCalls( - null, - $this->throwException(new Exception("Exception that we don't care about")), + ->with( + $this->bug->getSteps(), + $this->bug, + $this->callback(function (callable $exceptionCallback) use ($exception) { + if ($exception) { + $exceptionCallback($exception); + } + + return true; + }) ); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); + $this->bugRepository->expects($this->once())->method('find')->with(123)->willReturn($this->bug); + $this->bugRepository->expects($this->once())->method('startRecording')->with($this->bug); + $this->bugRepository->expects($this->once())->method('stopRecording')->with($this->bug); $this->helper->recordVideo(123); + $this->assertTrue($this->bug->isDebug()); + $this->assertSame($expectedVideoErrorMessage, $this->bug->getVideo()->getErrorMessage()); } - public function testRecordVideo(): void + public function exceptionProvider(): array { - $this->driver->expects($this->once())->method('quit'); - $this->selenoidHelper - ->expects($this->once()) - ->method('getCapabilities') - ->with($this->bug, true) - ->willReturn($this->capabilities); - $this->selenoidHelper - ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->stepRunner - ->expects($this->exactly(3)) - ->method('run') - ->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver); - $this->entityManager->expects($this->once())->method('find')->with(Bug::class, 123)->willReturn($this->bug); - $this->helper->recordVideo(123); + return [ + [null, null], + [new Exception('Something wrong'), null], + [new Exception('Something else wrong'), 'Something else wrong'], + ]; } } diff --git a/tests/Service/Bug/BugProgressTest.php b/tests/Service/Bug/BugProgressTest.php deleted file mode 100644 index 52e9f9ba..00000000 --- a/tests/Service/Bug/BugProgressTest.php +++ /dev/null @@ -1,88 +0,0 @@ -entityManager = $this->createMock(EntityManagerInterface::class); - $progress = new Progress(); - $progress->setTotal(10); - $progress->setProcessed(5); - $this->bug = new Bug(); - $this->bug->setProgress($progress); - } - - public function testIncreaseProcessed(): void - { - $this->entityManager->expects($this->once())->method('refresh')->with($this->bug); - $this->entityManager->expects($this->once())->method('lock')->with($this->bug, LockMode::PESSIMISTIC_WRITE); - $this->entityManager - ->expects($this->once()) - ->method('transactional') - ->with($this->callback(function ($callback) { - $callback(); - - return true; - })); - $bugProgress = new BugProgress($this->entityManager); - $bugProgress->increaseProcessed($this->bug, 2); - $this->assertSame(7, $this->bug->getProgress()->getProcessed()); - $this->assertSame(10, $this->bug->getProgress()->getTotal()); - } - - public function testIncreaseProcessedReachLimit(): void - { - $this->entityManager->expects($this->once())->method('refresh')->with($this->bug); - $this->entityManager->expects($this->once())->method('lock')->with($this->bug, LockMode::PESSIMISTIC_WRITE); - $this->entityManager - ->expects($this->once()) - ->method('transactional') - ->with($this->callback(function ($callback) { - $callback(); - - return true; - })); - $bugProgress = new BugProgress($this->entityManager); - $bugProgress->increaseProcessed($this->bug, 6); - $this->assertSame(10, $this->bug->getProgress()->getProcessed()); - $this->assertSame(10, $this->bug->getProgress()->getTotal()); - } - - public function testIncreaseTotal(): void - { - $this->entityManager->expects($this->once())->method('refresh')->with($this->bug); - $this->entityManager->expects($this->once())->method('lock')->with($this->bug, LockMode::PESSIMISTIC_WRITE); - $this->entityManager - ->expects($this->once()) - ->method('transactional') - ->with($this->callback(function ($callback) { - $callback(); - - return true; - })); - $bugProgress = new BugProgress($this->entityManager); - $bugProgress->increaseTotal($this->bug, 3); - $this->assertSame(5, $this->bug->getProgress()->getProcessed()); - $this->assertSame(13, $this->bug->getProgress()->getTotal()); - } -} diff --git a/tests/Service/Petrinet/MarkingHelperTest.php b/tests/Service/Petrinet/MarkingHelperTest.php index 7538d093..dfca208c 100644 --- a/tests/Service/Petrinet/MarkingHelperTest.php +++ b/tests/Service/Petrinet/MarkingHelperTest.php @@ -9,18 +9,27 @@ use SingleColorPetrinet\Builder\SingleColorPetrinetBuilder; use SingleColorPetrinet\Model\Color; use SingleColorPetrinet\Model\ColorfulFactory; +use SingleColorPetrinet\Model\ColorfulFactoryInterface; use SingleColorPetrinet\Model\Place; use Tienvx\Bundle\MbtBundle\Service\Petrinet\MarkingHelper; +use Tienvx\Bundle\MbtBundle\Service\Petrinet\MarkingHelperInterface; /** * @covers \Tienvx\Bundle\MbtBundle\Service\Petrinet\MarkingHelper */ class MarkingHelperTest extends TestCase { + protected ColorfulFactoryInterface $factory; + protected MarkingHelperInterface $helper; + + protected function setUp(): void + { + $this->factory = new ColorfulFactory(); + $this->helper = new MarkingHelper($this->factory); + } + public function testGetPlaces(): void { - $factory = new ColorfulFactory(); - $helper = new MarkingHelper($factory); $pm1 = new PlaceMarking(); $pm2 = new PlaceMarking(); $pm1->setPlace($p1 = new Place()); @@ -37,23 +46,21 @@ public function testGetPlaces(): void $this->assertSame([ 1 => 1, 2 => 3, - ], $helper->getPlaces($marking)); + ], $this->helper->getPlaces($marking)); } public function testGetMarking(): void { - $factory = new ColorfulFactory(); - $helper = new MarkingHelper($factory); - $builder = new SingleColorPetrinetBuilder($factory); + $builder = new SingleColorPetrinetBuilder($this->factory); $petrinet = $builder - ->connect($place1 = $builder->place(), $transition1 = $builder->transition()) - ->connect($transition1, $place2 = $builder->place()) - ->connect($place3 = $builder->place(), $transition2 = $builder->transition()) - ->connect($transition2, $place4 = $builder->place()) + ->connect($place1 = $builder->place(1), $transition1 = $builder->transition()) + ->connect($transition1, $place2 = $builder->place(2)) + ->connect($place3 = $builder->place(3), $transition2 = $builder->transition()) + ->connect($transition2, $place4 = $builder->place(4)) ->getPetrinet(); - $marking = $helper->getMarking($petrinet, [0 => 1, 2 => 3], new Color(['key' => 'value'])); + $marking = $this->helper->getMarking($petrinet, [1 => 1, 3 => 3], new Color(['key' => 'value'])); $this->assertSame(['key' => 'value'], $marking->getColor()->getValues()); $this->assertCount(1, $marking->getPlaceMarking($place1)->getTokens()); $this->assertNull($marking->getPlaceMarking($place2)); diff --git a/tests/Service/Petrinet/PetrinetHelperTest.php b/tests/Service/Petrinet/PetrinetHelperTest.php index dcd5a934..b6599f9f 100644 --- a/tests/Service/Petrinet/PetrinetHelperTest.php +++ b/tests/Service/Petrinet/PetrinetHelperTest.php @@ -2,11 +2,13 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Service\Petrinet; -use Petrinet\Model\PlaceInterface; +use Petrinet\Model\ArcInterface; use PHPUnit\Framework\TestCase; use SingleColorPetrinet\Model\Color; use SingleColorPetrinet\Model\ColorfulFactory; -use SingleColorPetrinet\Model\GuardedTransitionInterface; +use SingleColorPetrinet\Model\GuardedTransition as PetrinetTransition; +use SingleColorPetrinet\Model\Place as PetrinetPlace; +use Tienvx\AssignmentsEvaluator\AssignmentsEvaluator; use Tienvx\Bundle\MbtBundle\Entity\Model\Revision; use Tienvx\Bundle\MbtBundle\Service\ExpressionLanguage; use Tienvx\Bundle\MbtBundle\Service\Petrinet\PetrinetHelper; @@ -27,46 +29,118 @@ public function testBuild(): void { $factory = new ColorfulFactory(); $expressionLanguage = new ExpressionLanguage(); - $helper = new PetrinetHelper($factory, $expressionLanguage); - $places = [ + $assignmentsEvaluator = new AssignmentsEvaluator($expressionLanguage); + $helper = new PetrinetHelper($factory, $expressionLanguage, $assignmentsEvaluator); + + // Model revision + $revision = new Revision(); + $revision->setPlaces([ + $place0 = new Place(), $place1 = new Place(), $place2 = new Place(), - $place3 = new Place(), - ]; - $revision = new Revision(); - $revision->setPlaces($places); - $transitions = [ + ]); + $revision->setTransitions([ + $transition0 = new Transition(), $transition1 = new Transition(), $transition2 = new Transition(), - ]; + $transition3 = new Transition(), + ]); + $transition0->setFromPlaces([]); + $transition0->setToPlaces([0]); $transition1->setGuard('count > 0'); - $transition1->setFromPlaces([0, 1]); - $transition1->setToPlaces([2]); + $transition1->setFromPlaces([0]); + $transition1->setToPlaces([1, 2]); $transition2->setFromPlaces([2]); $transition2->setToPlaces([1]); - $revision->setTransitions($transitions); + $transition3->setExpression('count = count + 1; status = "open"'); + $transition3->setFromPlaces([1]); + $transition3->setToPlaces([0, 2]); + + // Petrinet $petrinet = $helper->build($revision); - $this->assertCount(3, $petrinet->getPlaces()); - foreach ($petrinet->getPlaces() as $place) { - $this->assertInstanceOf(PlaceInterface::class, $place); + $this->assertCount(4, $petrinet->getPlaces()); + $places = [ + 0 => [ + 'id' => -1, + 'input' => [], + 'output' => [0], + ], + 1 => [ + 'id' => 0, + 'input' => [0, 3], + 'output' => [1], + ], + 2 => [ + 'id' => 1, + 'input' => [1, 2], + 'output' => [3], + ], + 3 => [ + 'id' => 2, + 'input' => [1, 3], + 'output' => [2], + ], + ]; + foreach ($petrinet->getPlaces() as $index => $place) { + $this->assertInstanceOf(PetrinetPlace::class, $place); + $this->assertSame($places[$index]['id'], $place->getId()); + $this->assertSame( + $places[$index]['input'], + array_map(fn (ArcInterface $arc) => $arc->getTransition()->getId(), $place->getInputArcs()->toArray()), + ); + $this->assertSame( + $places[$index]['output'], + array_map(fn (ArcInterface $arc) => $arc->getTransition()->getId(), $place->getOutputArcs()->toArray()), + ); } - $this->assertCount(0, $petrinet->getPlaces()[0]->getInputArcs()); - $this->assertCount(1, $petrinet->getPlaces()[0]->getOutputArcs()); - $this->assertCount(1, $petrinet->getPlaces()[1]->getInputArcs()); - $this->assertCount(1, $petrinet->getPlaces()[1]->getOutputArcs()); - $this->assertCount(1, $petrinet->getPlaces()[2]->getInputArcs()); - $this->assertCount(1, $petrinet->getPlaces()[2]->getOutputArcs()); - $this->assertCount(2, $petrinet->getTransitions()); - foreach ($petrinet->getTransitions() as $place) { - $this->assertInstanceOf(GuardedTransitionInterface::class, $place); + $this->assertCount(4, $petrinet->getTransitions()); + $transitions = [ + 0 => [ + 'id' => 0, + 'input' => [-1], + 'output' => [0], + ], + 1 => [ + 'id' => 1, + 'input' => [0], + 'output' => [1, 2], + ], + 2 => [ + 'id' => 2, + 'input' => [2], + 'output' => [1], + ], + 3 => [ + 'id' => 3, + 'input' => [1], + 'output' => [0, 2], + ], + ]; + foreach ($petrinet->getTransitions() as $index => $transition) { + $this->assertInstanceOf(PetrinetTransition::class, $transition); + $this->assertSame($transitions[$index]['id'], $transition->getId()); + $this->assertSame( + $transitions[$index]['input'], + array_map(fn (ArcInterface $arc) => $arc->getPlace()->getId(), $transition->getInputArcs()->toArray()), + ); + $this->assertSame( + $transitions[$index]['output'], + array_map(fn (ArcInterface $arc) => $arc->getPlace()->getId(), $transition->getOutputArcs()->toArray()), + ); + if (1 === $index) { + $this->assertIsCallable($guardCallback = $transition->getGuard()); + $this->assertTrue($guardCallback(new Color(['count' => 1]))); + $this->assertFalse($guardCallback(new Color(['count' => 0]))); + } else { + $this->assertNull($transition->getGuard()); + } + if (3 === $index) { + $this->assertIsCallable($expressionCallback = $transition->getExpression()); + $this->assertSame(['count' => 2, 'status' => 'open'], $expressionCallback(new Color(['count' => 1]))); + $this->assertSame(['count' => 1, 'status' => 'open'], $expressionCallback(new Color(['count' => 0]))); + } else { + $this->assertNull($transition->getExpression()); + } } - $this->assertIsCallable($guardCallback = $petrinet->getTransitions()[0]->getGuard()); - $this->assertTrue($guardCallback(new Color(['count' => 1]))); - $this->assertFalse($guardCallback(new Color(['count' => 0]))); - $this->assertNull($petrinet->getTransitions()[1]->getGuard()); - $this->assertCount(2, $petrinet->getTransitions()[0]->getInputArcs()); - $this->assertCount(1, $petrinet->getTransitions()[0]->getOutputArcs()); - $this->assertCount(1, $petrinet->getTransitions()[1]->getInputArcs()); - $this->assertCount(1, $petrinet->getTransitions()[1]->getOutputArcs()); } } diff --git a/tests/Service/SelenoidHelperTest.php b/tests/Service/SelenoidHelperTest.php index 0031550b..4b37f382 100644 --- a/tests/Service/SelenoidHelperTest.php +++ b/tests/Service/SelenoidHelperTest.php @@ -3,6 +3,8 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Service; use Facebook\WebDriver\Remote\DesiredCapabilities; +use Facebook\WebDriver\Remote\RemoteWebDriver; +use PHPUnit\Framework\TestCase; use Tienvx\Bundle\MbtBundle\Entity\Bug; use Tienvx\Bundle\MbtBundle\Entity\Task; use Tienvx\Bundle\MbtBundle\Entity\Task\Browser; @@ -10,17 +12,18 @@ use Tienvx\Bundle\MbtBundle\Model\TaskInterface; use Tienvx\Bundle\MbtBundle\Service\SelenoidHelper; use Tienvx\Bundle\MbtBundle\Service\SelenoidHelperInterface; -use Tienvx\Bundle\MbtBundle\Tests\StepsTestCase; /** + * @covers \Tienvx\Bundle\MbtBundle\Service\SelenoidHelper + * * @uses \Tienvx\Bundle\MbtBundle\Entity\Task * @uses \Tienvx\Bundle\MbtBundle\Model\Task * @uses \Tienvx\Bundle\MbtBundle\Entity\Bug * @uses \Tienvx\Bundle\MbtBundle\Model\Bug * @uses \Tienvx\Bundle\MbtBundle\Model\Task\Browser - * @covers \Tienvx\Bundle\MbtBundle\Service\SelenoidHelper + * @uses \Tienvx\Bundle\MbtBundle\Model\Debug */ -class SelenoidHelperTest extends StepsTestCase +class SelenoidHelperTest extends TestCase { protected string $webdriverUri = 'http://localhost:4444'; protected SelenoidHelperInterface $selenoidHelper; @@ -30,7 +33,10 @@ class SelenoidHelperTest extends StepsTestCase protected function setUp(): void { - $this->selenoidHelper = new SelenoidHelper(); + $this->selenoidHelper = $this->createPartialMock( + SelenoidHelper::class, + ['createDriverInternal', 'waitForVideoContainer'] + ); $this->selenoidHelper->setWebdriverUri($this->webdriverUri); $browser = new Browser(); $browser->setName('firefox'); @@ -46,11 +52,11 @@ protected function setUp(): void public function testGetLogUrl(): void { $this->assertSame( - 'http://localhost:4444/logs/task-123.log', + $this->webdriverUri . '/logs/task-123.log', $this->selenoidHelper->getLogUrl($this->task) ); $this->assertSame( - 'http://localhost:4444/logs/bug-234.log', + $this->webdriverUri . '/logs/bug-234.log', $this->selenoidHelper->getLogUrl($this->bug) ); } @@ -58,74 +64,62 @@ public function testGetLogUrl(): void public function testGetVideoUrl(): void { $this->assertSame( - 'http://localhost:4444/video/task-123.mp4', + $this->webdriverUri . '/video/task-123.mp4', $this->selenoidHelper->getVideoUrl($this->task) ); $this->assertSame( - 'http://localhost:4444/video/bug-234.mp4', + $this->webdriverUri . '/video/bug-234.mp4', $this->selenoidHelper->getVideoUrl($this->bug) ); } /** - * @dataProvider capabilitiesParameterProvider + * @dataProvider entityProvider */ - public function testGetCapabilities(bool $debug, bool $hasBug): void + public function testCreateDriver(string $type, bool $debug): void { - $this->assertCapabilities( - $this->selenoidHelper->getCapabilities($hasBug ? $this->bug : $this->task, $debug), - $debug, - $hasBug - ); + $this->{$type}->setDebug($debug); + $driver = $this->createMock(RemoteWebDriver::class); + $this->selenoidHelper + ->expects($this->once()) + ->method('createDriverInternal') + ->with( + $this->webdriverUri . '/wd/hub', + $this->callback(function (DesiredCapabilities $capabilities) use ($type, $debug) { + $this->assertCapabilities($capabilities, $type, $debug); + + return true; + }) + ) + ->willReturn($driver); + $this->selenoidHelper->expects($this->exactly($debug))->method('waitForVideoContainer'); + $this->assertSame($driver, $this->selenoidHelper->createDriver($this->{$type})); } - private function assertCapabilities(DesiredCapabilities $capabilities, bool $debug, bool $hasBug): void + private function assertCapabilities(DesiredCapabilities $capabilities, string $type, bool $debug): void { $this->assertSame($debug, $capabilities->is('enableVideo')); $this->assertSame($debug, $capabilities->is('enableLog')); $this->assertSame($this->task->getBrowser()->getVersion(), $capabilities->getVersion()); $this->assertSame($this->task->getBrowser()->getName(), $capabilities->getBrowserName()); $this->assertSame( - $debug ? ($hasBug ? "bug-{$this->bug->getId()}.log" : "task-{$this->task->getId()}.log") : null, + $debug ? "$type-{$this->{$type}->getId()}.log" : null, $capabilities->getCapability('logName') ); $this->assertSame( - $debug ? ($hasBug ? "bug-{$this->bug->getId()}.mp4" : "task-{$this->task->getId()}.mp4") : null, + $debug ? "$type-{$this->{$type}->getId()}.mp4" : null, $capabilities->getCapability('videoName') ); + $this->assertSame($debug ? 60 : null, $capabilities->getCapability('videoFrameRate')); } - public function capabilitiesParameterProvider(): array + public function entityProvider(): array { return [ - [true, false], - [true, true], - [false, false], - [false, true], + ['task', false], + ['task', true], + ['bug', false], + ['bug', true], ]; } - - public function testGetLogName(): void - { - $this->assertSame( - 'task-123.log', - $this->selenoidHelper->getLogName($this->task) - ); - $this->assertSame( - 'bug-234.log', - $this->selenoidHelper->getLogName($this->bug) - ); - } - - public function testGetVideoName(): void - { - $this->assertSame( - 'task-123.mp4', - $this->selenoidHelper->getVideoName($this->task) - ); - $this->assertSame( - 'bug-234.mp4', - $this->selenoidHelper->getVideoName($this->bug) - ); - } } diff --git a/tests/Service/Step/Builder/PetrinetDomainLogicTest.php b/tests/Service/Step/Builder/PetrinetDomainLogicTest.php new file mode 100644 index 00000000..aa273cf5 --- /dev/null +++ b/tests/Service/Step/Builder/PetrinetDomainLogicTest.php @@ -0,0 +1,166 @@ +transitionService = $this->createMock(GuardedTransitionServiceInterface::class); + $this->markingHelper = $this->createMock(MarkingHelperInterface::class); + $this->petrinet = $this->createMock(PetrinetInterface::class); + $this->petrinetDomainLogic = new PetrinetDomainLogic( + $this->transitionService, + $this->markingHelper, + $this->petrinet + ); + $this->transitions = [ + $transition1 = new GuardedTransition(), + $transition2 = new GuardedTransition(), + $transition3 = new GuardedTransition(), + $transition4 = new GuardedTransition(), + ]; + $transition1->setId(0); + $transition2->setId(1); + $transition3->setId(2); + $transition4->setId(3); + $this->markings = [ + $marking1 = new ColorfulMarking(), + $marking2 = new ColorfulMarking(), + $marking3 = new ColorfulMarking(), + $marking4 = new ColorfulMarking(), + $marking5 = new ColorfulMarking(), + ]; + $marking2->setColor(new Color(['key 1' => 'value 1'])); + $marking3->setColor(new Color(['key 2' => 'value 2'])); + $marking4->setColor(new Color(['key 3' => 'value 3'])); + $marking5->setColor(new Color(['key 4' => 'value 4'])); + $this->places = [ + [1 => 11], + [2 => 22], + [3 => 33], + [4 => 44], + ]; + } + + /** + * @dataProvider invalidNodeProvider + */ + public function testCalculateEstimatedCostInvalidNode(mixed $fromNode, mixed $toNode): void + { + $this->expectExceptionObject(new RuntimeException('The provided nodes are invalid')); + $this->petrinetDomainLogic->calculateEstimatedCost($fromNode, $toNode); + } + + public function invalidNodeProvider(): array + { + $step = new Step([], new Color(), 0); + + return [ + [$step, 'to node'], + ['from node', $step], + ['from node', 'to node'], + ]; + } + + /** + * @dataProvider estimatedCostProvider + */ + public function testCalculateEstimatedCost(Step $fromNode, Step $toNode, int $cost): void + { + $this->assertSame($cost, $this->petrinetDomainLogic->calculateEstimatedCost($fromNode, $toNode)); + } + + public function estimatedCostProvider(): array + { + $color = new Color(['key' => 'value']); + + return [ + [$this->getStep([0 => 1, 1 => 5, 2 => 3], $color), $this->getStep([], $color), 9], + [$this->getStep([], $color), $this->getStep([0 => 3, 1 => 2, 2 => 2, 3 => 1], $color), 8], + [$this->getStep([0 => 2, 1 => 4], $color), $this->getStep([1 => 5, 2 => 3, 3 => 1], $color), 7], + ]; + } + + public function testCalculateRealCost(): void + { + $this->assertSame(1, $this->petrinetDomainLogic->calculateRealCost('node', 'adjacent')); + } + + public function testGetAdjacentNodesOfInvalidNode(): void + { + $this->expectExceptionObject(new RuntimeException('The provided node is invalid')); + $this->petrinetDomainLogic->getAdjacentNodes('invalid'); + } + + public function testGetAdjacentNodes(): void + { + $node = $this->getStep([12 => 34], new Color(['key' => 'value'])); + $this->markingHelper + ->expects($this->exactly(count($this->transitions) + 1)) + ->method('getMarking') + ->with($this->petrinet, $node->getPlaces(), $node->getColor()) + ->willReturnOnConsecutiveCalls(...$this->markings); + $this->transitionService + ->expects($this->once()) + ->method('getEnabledTransitions') + ->with($this->petrinet, $this->markings[0]) + ->willReturn($this->transitions); + $this->transitionService + ->expects($this->exactly(count($this->transitions))) + ->method('fire') + ->withConsecutive(...array_map( + fn (int $index) => [$this->transitions[$index], $this->markings[$index + 1]], + array_keys($this->transitions) + )); + $this->markingHelper + ->expects($this->exactly(count($this->transitions))) + ->method('getPlaces') + ->withConsecutive(...array_map( + fn (MarkingInterface $marking) => [$marking], + array_slice($this->markings, 1, count($this->transitions)) + )) + ->willReturnOnConsecutiveCalls(...$this->places); + $adjacents = $this->petrinetDomainLogic->getAdjacentNodes($node); + $this->assertCount(count($this->transitions), $adjacents); + foreach ($adjacents as $index => $adjacent) { + $this->assertInstanceOf(StepInterface::class, $adjacent); + $this->assertSame($this->places[$index], $adjacent->getPlaces()); + $this->assertSame($this->markings[$index + 1]->getColor(), $adjacent->getColor()); + $this->assertSame($this->transitions[$index]->getId(), $adjacent->getTransition()); + } + } + + protected function getStep(array $places, ColorInterface $color): StepInterface + { + return new Step($places, $color, 0); + } +} diff --git a/tests/Service/ShortestPathStepsBuilderTest.php b/tests/Service/Step/Builder/ShortestPathStepsBuilderTest.php similarity index 83% rename from tests/Service/ShortestPathStepsBuilderTest.php rename to tests/Service/Step/Builder/ShortestPathStepsBuilderTest.php index cff89022..cbd84e28 100644 --- a/tests/Service/ShortestPathStepsBuilderTest.php +++ b/tests/Service/Step/Builder/ShortestPathStepsBuilderTest.php @@ -1,38 +1,41 @@ initPlaces($builder); $this->initTransitions($builder); $this->initPetrinet($builder); - $this->initPetrinetDomainLogic(); $this->initBug(); $this->initStepsBuilder(); + $this->initResults(); } protected function initPlaces(SingleColorPetrinetBuilder $builder): void @@ -159,12 +163,6 @@ protected function initPetrinet(SingleColorPetrinetBuilder $builder): void ->getPetrinet(); } - protected function initPetrinetDomainLogic(): void - { - $this->petrinetDomainLogic = new PetrinetDomainLogic($this->transitionService, $this->markingHelper); - $this->petrinetDomainLogic->setPetrinet($this->petrinet); - } - protected function initBug(): void { $this->bug = new Bug(); @@ -193,26 +191,24 @@ protected function initBug(): void $this->bug->setTask($task); } - protected function geColor(int $number): Color + protected function geColor(int $products): Color { - return new Color(['products' => $number]); + return new Color(['products' => $products]); } protected function initStepsBuilder(): void { $this->petrinetHelper = $this->createMock(PetrinetHelperInterface::class); - $this->petrinetHelper - ->expects($this->once()) - ->method('build') - ->with($this->revision) - ->willReturn($this->petrinet); - $this->stepsBuilder = new ShortestPathStepsBuilder($this->petrinetHelper, $this->petrinetDomainLogic); + $this->stepsBuilder = new ShortestPathStepsBuilder( + $this->petrinetHelper, + $this->transitionService, + $this->markingHelper + ); } - public function testGetShortestPathFromCartEmptyToCheckout(): void + protected function initResults(): void { - $nodes = $this->stepsBuilder->create($this->bug, 0, 12); - $this->assertNodes([ + $this->fromEmptyCartToConfirmOrderNodes = [ [ 'transition' => $this->clearCart->getId(), 'places' => [$this->cartEmpty->getId() => 1], @@ -248,11 +244,63 @@ public function testGetShortestPathFromCartEmptyToCheckout(): void 'places' => [$this->order->getId() => 1], 'color' => ['products' => 1], ], - ], $nodes); + ]; + } + + protected function expectsPetrinetHelper(): void + { + $this->petrinetHelper + ->expects($this->once()) + ->method('build') + ->with($this->revision) + ->willReturn($this->petrinet); + } + + /** + * @dataProvider invalidRangeProvider + */ + public function testGetInvalidRange(int $from, int $to): void + { + $this->expectExceptionObject(new OutOfRangeException('Can not create shortest steps between invalid range')); + iterator_to_array($this->stepsBuilder->create($this->bug, $from, $to)); + } + + public function invalidRangeProvider(): array + { + $validMinFrom = 0; + $validMaxTo = 16; + + return [ + [-1, $validMaxTo], + [$validMinFrom, 17], + [-1, 17], + ]; + } + + public function testGetShortestPathFromCartEmptyToCheckout(): void + { + $this->expectsPetrinetHelper(); + $nodes = $this->stepsBuilder->create($this->bug, 0, 12); + $this->assertNodes($this->fromEmptyCartToConfirmOrderNodes, $nodes); + } + + public function testGetShortestPathBetweenSameSteps(): void + { + $this->expectsPetrinetHelper(); + $nodes = $this->stepsBuilder->create($this->bug, 0, 10); + $this->assertNodes($this->fromEmptyCartToConfirmOrderNodes, $nodes); + } + + public function testGetShortestPathFromCartEmptyToFourProductsInCart(): void + { + $this->expectsPetrinetHelper(); + $this->expectExceptionObject(new StepsNotConnectedException('Can not connect remaining steps')); + iterator_to_array($this->stepsBuilder->create($this->bug, 0, 6)); } public function testGetShortestPathFromCartHasProductsToShipping(): void { + $this->expectsPetrinetHelper(); $nodes = $this->stepsBuilder->create($this->bug, 4, 14); $this->assertNodes([ [ @@ -280,40 +328,30 @@ public function testGetShortestPathFromCartHasProductsToShipping(): void 'places' => [$this->cartHasProducts->getId() => 1], 'color' => ['products' => 4], ], - [ - 'transition' => $this->clearCart->getId(), - 'places' => [$this->cartEmpty->getId() => 1], - 'color' => ['products' => 0], - ], - [ - 'transition' => $this->addFirstProduct->getId(), - 'places' => [$this->cartHasProducts->getId() => 1], - 'color' => ['products' => 1], - ], [ 'transition' => $this->goToCheckout->getId(), 'places' => [$this->checkout->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], [ 'transition' => $this->fillAddress->getId(), 'places' => [$this->address->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], [ 'transition' => $this->chooseShipping->getId(), 'places' => [$this->shipping->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], [ 'transition' => $this->choosePayment->getId(), 'places' => [$this->payment->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], [ 'transition' => $this->confirmOrder->getId(), 'places' => [$this->order->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], ], $nodes); } diff --git a/tests/Service/Step/Runner/BugStepsRunnerTest.php b/tests/Service/Step/Runner/BugStepsRunnerTest.php new file mode 100644 index 00000000..ee67a662 --- /dev/null +++ b/tests/Service/Step/Runner/BugStepsRunnerTest.php @@ -0,0 +1,35 @@ +stepsRunner = new BugStepsRunner($this->selenoidHelper, $this->stepRunner); + } + + protected function assertHandlingException(Exception $exception, array $bugSteps = []): void + { + $this->handleException + ->expects($this->once()) + ->method('__invoke') + ->with($exception); + } +} diff --git a/tests/Service/Step/Runner/ExploreStepsRunnerTest.php b/tests/Service/Step/Runner/ExploreStepsRunnerTest.php new file mode 100644 index 00000000..6275eeaf --- /dev/null +++ b/tests/Service/Step/Runner/ExploreStepsRunnerTest.php @@ -0,0 +1,66 @@ +config = $this->createMock(ConfigInterface::class); + $this->stepsRunner = new ExploreStepsRunner($this->selenoidHelper, $this->stepRunner, $this->config); + } + + /** + * @dataProvider entityProvider + */ + public function testRunReachMax2Steps(DebugInterface $entity): void + { + $this->selenoidHelper + ->expects($this->once()) + ->method('createDriver') + ->with($entity) + ->willReturn($this->driver); + $this->driver->expects($this->once())->method('quit'); + $this->assertRunSteps(array_slice($this->steps, 0, 2), null, 2); + $this->handleException->expects($this->never())->method('__invoke'); + $this->stepsRunner->run($this->steps, $entity, $this->handleException); + } + + protected function assertHandlingException(Exception $exception, array $bugSteps = []): void + { + $this->handleException + ->expects($this->once()) + ->method('__invoke') + ->with($exception, $bugSteps); + } + + protected function assertRunSteps(array $steps = [], ?Exception $exception = null, int $maxSteps = 99): void + { + parent::assertRunSteps($steps, $exception); + $this->config + ->expects($this->exactly($exception ? count($steps) - 1 : count($steps))) + ->method('getMaxSteps') + ->willReturn($maxSteps); + } +} diff --git a/tests/Service/Step/Runner/StepRunnerTest.php b/tests/Service/Step/Runner/StepRunnerTest.php new file mode 100644 index 00000000..342fc16a --- /dev/null +++ b/tests/Service/Step/Runner/StepRunnerTest.php @@ -0,0 +1,123 @@ +driver = $this->createMock(RemoteWebDriver::class); + $this->revision = new Revision(); + $this->color = $this->createMock(ColorInterface::class); + $transitions = [ + $transition = new Transition(), + ]; + $transition->setCommands([ + $command1 = CommandFactory::create('open'), + $command2 = CommandFactory::create('click'), + ]); + $this->revision->setTransitions($transitions); + $places = [ + $place1 = new Place(), + $place2 = new Place(), + ]; + $place1->setCommands([ + $command3 = CommandFactory::create('assertEditable'), + $command4 = CommandFactory::create('assertAlert'), + ]); + $place2->setCommands([ + $command5 = CommandFactory::create('assertText'), + ]); + $this->revision->setPlaces($places); + $assertValuesInstance = function (ValuesInterface $values) { + if (!in_array($values, $this->valuesInstances, true)) { + $this->valuesInstances[] = $values; + } + + return true; + }; + $this->commands = [ + [ + $command1->getCommand(), + $command1->getTarget(), + $command1->getValue(), + $this->callback($assertValuesInstance), + $this->driver, + ], + [ + $command2->getCommand(), + $command2->getTarget(), + $command2->getValue(), + $this->callback($assertValuesInstance), + $this->driver, + ], + [ + $command3->getCommand(), + $command3->getTarget(), + $command3->getValue(), + $this->callback($assertValuesInstance), + $this->driver, + ], + [ + $command4->getCommand(), + $command4->getTarget(), + $command4->getValue(), + $this->callback($assertValuesInstance), + $this->driver, + ], + [ + $command5->getCommand(), + $command5->getTarget(), + $command5->getValue(), + $this->callback($assertValuesInstance), + $this->driver, + ], + ]; + + $this->commandManager = $this->createMock(CommandManager::class); + } + + public function testRun(): void + { + $step = new Step([0 => 1, 1 => 1], $this->color, 0); + $this->commandManager + ->expects($this->exactly(5)) + ->method('run') + ->withConsecutive(...$this->commands); + $stepRunner = new StepRunner($this->commandManager); + $stepRunner->run($step, $this->revision, $this->driver); + $this->assertCount(3, $this->valuesInstances); + } +} diff --git a/tests/Service/Step/Runner/StepsRunnerTestCase.php b/tests/Service/Step/Runner/StepsRunnerTestCase.php new file mode 100644 index 00000000..d56fe260 --- /dev/null +++ b/tests/Service/Step/Runner/StepsRunnerTestCase.php @@ -0,0 +1,160 @@ +revision = new Revision(); + $this->task = new Task(); + $this->task->setModelRevision($this->revision); + $this->bug = new Bug(); + $this->bug->setTask($this->task); + } + + protected function setUp(): void + { + $this->steps = [ + new Step([], new Color(), 0), + new Step([], new Color(), 1), + new Step([], new Color(), 2), + new Step([], new Color(), 3), + ]; + $this->stepRunner = $this->createMock(StepRunnerInterface::class); + $this->selenoidHelper = $this->createMock(SelenoidHelperInterface::class); + $this->driver = $this->createMock(RemoteWebDriver::class); + $this->handleException = $this->getMockBuilder(\stdClass::class) + ->addMethods(['__invoke']) + ->getMock(); + } + + /** + * @dataProvider entityProvider + */ + public function testRunCanNotCreateDriver(DebugInterface $entity): void + { + $exception = new Exception('can not create driver'); + $this->selenoidHelper + ->expects($this->once()) + ->method('createDriver') + ->with($entity) + ->willThrowException($exception); + $this->driver->expects($this->never())->method('quit'); + $this->assertRunSteps(); + $this->assertHandlingException($exception); + $this->stepsRunner->run($this->steps, $entity, $this->handleException); + } + + /** + * @dataProvider entityProvider + */ + public function testRunInvalidSteps(DebugInterface $entity): void + { + $this->expectExceptionObject( + new UnexpectedValueException(sprintf('Step must be instance of "%s".', StepInterface::class)) + ); + $this->selenoidHelper + ->expects($this->once()) + ->method('createDriver') + ->with($entity) + ->willReturn($this->driver); + $this->driver->expects($this->once())->method('quit'); + $this->assertRunSteps(); + $this->handleException->expects($this->never())->method('__invoke'); + $this->stepsRunner->run([new \stdClass()], $entity, $this->handleException); + } + + /** + * @dataProvider entityProvider + */ + public function testRunNotFoundBug(DebugInterface $entity): void + { + $this->selenoidHelper + ->expects($this->once()) + ->method('createDriver') + ->with($entity) + ->willReturn($this->driver); + $this->driver->expects($this->once())->method('quit'); + $this->assertRunSteps($this->steps); + $this->handleException->expects($this->never())->method('__invoke'); + $this->stepsRunner->run($this->steps, $entity, $this->handleException); + } + + /** + * @dataProvider entityProvider + */ + public function testRunFoundBugAtThirdStep(DebugInterface $entity): void + { + $bugSteps = array_slice($this->steps, 0, 3); + $exception = new Exception('Can not run the third step'); + $this->selenoidHelper + ->expects($this->once()) + ->method('createDriver') + ->with($entity) + ->willReturn($this->driver); + $this->driver->expects($this->once())->method('quit'); + $this->assertRunSteps($bugSteps, $exception); + $this->assertHandlingException($exception, $bugSteps); + $this->stepsRunner->run($this->steps, $entity, $this->handleException); + } + + abstract protected function assertHandlingException(Exception $exception, array $bugSteps = []): void; + + protected function assertRunSteps(array $steps = [], ?Exception $exception = null): void + { + if ($steps) { + $mock = $this->stepRunner + ->expects($this->exactly(count($steps))) + ->method('run') + ->withConsecutive(...array_map( + fn (StepInterface $step) => [$step, $this->revision, $this->driver], + $steps + )); + if ($exception) { + $mock->will($this->onConsecutiveCalls( + ...[...array_fill(0, count($steps) - 1, null), $this->throwException($exception)], + )); + } + } else { + $this->stepRunner->expects($this->never())->method('run'); + } + } + + public function entityProvider(): array + { + return [ + [$this->task], + [$this->bug], + ]; + } +} diff --git a/tests/Service/StepRunnerTest.php b/tests/Service/StepRunnerTest.php deleted file mode 100644 index 78f7935a..00000000 --- a/tests/Service/StepRunnerTest.php +++ /dev/null @@ -1,81 +0,0 @@ -driver = $this->createMock(RemoteWebDriver::class); - $this->revision = new Revision(); - $this->color = $this->createMock(ColorInterface::class); - $transitions = [ - $transition = new Transition(), - ]; - $transition->setCommands([ - $command1 = CommandFactory::create(WindowCommandRunner::OPEN), - $command2 = CommandFactory::create(MouseCommandRunner::CLICK), - ]); - $this->revision->setTransitions($transitions); - $places = [ - $place1 = new Place(), - $place2 = new Place(), - ]; - $place1->setCommands([ - $command3 = CommandFactory::create(AssertionRunner::ASSERT_EDITABLE), - $command4 = CommandFactory::create(AssertionRunner::ASSERT_ALERT), - ]); - $place2->setCommands([ - $command5 = CommandFactory::create(AssertionRunner::ASSERT_TEXT), - ]); - $this->revision->setPlaces($places); - $this->commands = [ - [$command1, $this->color, $this->driver], - [$command2, $this->color, $this->driver], - [$command3, $this->color, $this->driver], - [$command4, $this->color, $this->driver], - [$command5, $this->color, $this->driver], - ]; - - $this->commandRunnerManager = $this->createMock(CommandRunnerManager::class); - } - - public function testRun(): void - { - $step = new Step([0 => 1, 1 => 1], $this->color, 0); - $this->commandRunnerManager->expects($this->exactly(5))->method('run')->withConsecutive(...$this->commands); - $stepRunner = new StepRunner($this->commandRunnerManager); - $stepRunner->run($step, $this->revision, $this->driver); - } -} diff --git a/tests/Service/Task/TaskHelperTest.php b/tests/Service/Task/TaskHelperTest.php index 69d6f2ff..4bd4821b 100644 --- a/tests/Service/Task/TaskHelperTest.php +++ b/tests/Service/Task/TaskHelperTest.php @@ -2,28 +2,24 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Service\Task; -use Doctrine\DBAL\Connection; -use Doctrine\ORM\EntityManagerInterface; use Exception; -use Facebook\WebDriver\Remote\DesiredCapabilities; -use Facebook\WebDriver\Remote\RemoteWebDriver; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; use SingleColorPetrinet\Model\Color; -use Tienvx\Bundle\MbtBundle\Entity\Bug; -use Tienvx\Bundle\MbtBundle\Entity\Model\Revision; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException; +use Symfony\Component\Messenger\MessageBusInterface; use Tienvx\Bundle\MbtBundle\Entity\Task; -use Tienvx\Bundle\MbtBundle\Exception\RuntimeException; use Tienvx\Bundle\MbtBundle\Exception\UnexpectedValueException; use Tienvx\Bundle\MbtBundle\Generator\GeneratorInterface; use Tienvx\Bundle\MbtBundle\Generator\GeneratorManagerInterface; -use Tienvx\Bundle\MbtBundle\Model\Bug\StepInterface; +use Tienvx\Bundle\MbtBundle\Message\CreateBugMessage; use Tienvx\Bundle\MbtBundle\Model\TaskInterface; -use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelperInterface; +use Tienvx\Bundle\MbtBundle\Repository\TaskRepositoryInterface; use Tienvx\Bundle\MbtBundle\Service\ConfigInterface; -use Tienvx\Bundle\MbtBundle\Service\SelenoidHelperInterface; -use Tienvx\Bundle\MbtBundle\Service\StepRunnerInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Runner\ExploreStepsRunner; use Tienvx\Bundle\MbtBundle\Service\Task\TaskHelper; use Tienvx\Bundle\MbtBundle\Service\Task\TaskHelperInterface; -use Tienvx\Bundle\MbtBundle\Tests\StepsTestCase; use Tienvx\Bundle\MbtBundle\ValueObject\Bug\Step; /** @@ -35,21 +31,18 @@ * @uses \Tienvx\Bundle\MbtBundle\Entity\Bug * @uses \Tienvx\Bundle\MbtBundle\Model\Bug * @uses \Tienvx\Bundle\MbtBundle\Model\Bug\Step + * @uses \Tienvx\Bundle\MbtBundle\Model\Debug + * @uses \Tienvx\Bundle\MbtBundle\Message\CreateBugMessage */ -class TaskHelperTest extends StepsTestCase +class TaskHelperTest extends TestCase { protected array $steps; - protected GeneratorManagerInterface $generatorManager; - protected EntityManagerInterface $entityManager; - protected StepRunnerInterface $stepRunner; - protected BugHelperInterface $bugHelper; + protected GeneratorManagerInterface|MockObject $generatorManager; + protected TaskRepositoryInterface|MockObject $taskRepository; + protected MessageBusInterface|MockObject $messageBus; + protected ExploreStepsRunner|MockObject $stepsRunner; protected TaskHelperInterface $taskHelper; - protected SelenoidHelperInterface $selenoidHelper; - protected ConfigInterface $config; - protected Connection $connection; - protected DesiredCapabilities $capabilities; - protected RemoteWebDriver $driver; - protected Revision $revision; + protected ConfigInterface|MockObject $config; protected TaskInterface $task; protected function setUp(): void @@ -61,151 +54,88 @@ protected function setUp(): void new Step([], new Color(), 3), ]; $this->generatorManager = $this->createMock(GeneratorManagerInterface::class); - $this->entityManager = $this->createMock(EntityManagerInterface::class); - $this->stepRunner = $this->createMock(StepRunnerInterface::class); - $this->bugHelper = $this->createMock(BugHelperInterface::class); - $this->selenoidHelper = $this->createMock(SelenoidHelperInterface::class); + $this->taskRepository = $this->createMock(TaskRepositoryInterface::class); + $this->messageBus = $this->createMock(MessageBusInterface::class); + $this->stepsRunner = $this->createMock(ExploreStepsRunner::class); $this->config = $this->createMock(ConfigInterface::class); - $this->connection = $this->createMock(Connection::class); $this->taskHelper = new TaskHelper( $this->generatorManager, - $this->entityManager, - $this->stepRunner, - $this->bugHelper, - $this->selenoidHelper, + $this->taskRepository, + $this->messageBus, + $this->stepsRunner, $this->config ); - $this->driver = $this->createMock(RemoteWebDriver::class); - $this->capabilities = new DesiredCapabilities(); - $this->revision = new Revision(); $this->task = new Task(); $this->task->setId(123); $this->task->setRunning(false); - $this->task->setModelRevision($this->revision); + $this->task->setDebug(true); } public function testRunNoTask(): void { $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('Can not execute task 123: task not found'); - $this->entityManager->expects($this->once())->method('find')->with(Task::class, 123)->willReturn(null); + $this->expectExceptionMessage('Can not run task 123: task not found'); + $this->taskRepository->expects($this->once())->method('find')->with(123)->willReturn(null); $this->taskHelper->run(123); } - public function testRunAlreadyRunningTask(): void + public function testRunTaskAlreadyRunning(): void { - $task = new Task(); - $task->setId(123); - $task->setRunning(true); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Task 123 is already running'); - $this->entityManager->expects($this->once())->method('find')->with(Task::class, 123)->willReturn($task); + $this->expectException(RecoverableMessageHandlingException::class); + $this->expectExceptionMessage('Can not run task 123: task is running. Will retry later'); + $this->task->setRunning(true); + $this->taskRepository->expects($this->once())->method('find')->with(123)->willReturn($this->task); $this->taskHelper->run(123); } - public function testRun(): void + /** + * @dataProvider exceptionProvider + */ + public function testRun(?Exception $exception): void { - $this->config->expects($this->exactly(4))->method('getMaxSteps')->willReturn(150); + $this->taskRepository->expects($this->once())->method('find')->with(123)->willReturn($this->task); + $this->taskRepository->expects($this->once())->method('startRunning')->with($this->task); + $this->taskRepository->expects($this->once())->method('stopRunning')->with($this->task); $generator = $this->createMock(GeneratorInterface::class); - $generator->expects($this->once())->method('generate')->with($this->task)->willReturnCallback( - fn () => yield from $this->steps - ); - $this->driver->expects($this->once())->method('quit'); - $this->config->expects($this->once())->method('getGenerator')->willReturn('random'); + $generator->expects($this->once())->method('generate')->with($this->task)->willReturn($this->steps); $this->generatorManager->expects($this->once())->method('getGenerator')->with('random')->willReturn($generator); - $this->selenoidHelper - ->expects($this->once()) - ->method('getCapabilities') - ->with($this->task, $this->task->isDebug()) - ->willReturn($this->capabilities); - $this->selenoidHelper - ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->stepRunner - ->expects($this->exactly(4)) - ->method('run') - ->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver); - $this->entityManager->expects($this->once())->method('find')->with(Task::class, 123)->willReturn($this->task); - $this->entityManager->expects($this->exactly(2))->method('flush'); - $this->connection->expects($this->once())->method('connect'); - $this->entityManager->expects($this->once())->method('getConnection')->willReturn($this->connection); - $this->entityManager->expects($this->never())->method('persist'); - $this->taskHelper->run(123); - $this->assertFalse($this->task->isRunning()); - } - - public function testRunFoundBug(): void - { - $this->config->expects($this->exactly(2))->method('getMaxSteps')->willReturn(150); - $generator = $this->createMock(GeneratorInterface::class); - $generator->expects($this->once())->method('generate')->with($this->task)->willReturnCallback( - fn () => yield from $this->steps - ); - $this->driver->expects($this->once())->method('quit'); $this->config->expects($this->once())->method('getGenerator')->willReturn('random'); - $this->generatorManager->expects($this->once())->method('getGenerator')->with('random')->willReturn($generator); - $this->selenoidHelper + $this->stepsRunner ->expects($this->once()) - ->method('getCapabilities') - ->with($this->task, $this->task->isDebug()) - ->willReturn($this->capabilities); - $this->selenoidHelper - ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->stepRunner->expects($this->exactly(3))->method('run') - ->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver) - ->will($this->onConsecutiveCalls( - null, - null, - $this->throwException(new Exception('Can not run the third step')), - )); - $this->entityManager->expects($this->once())->method('find')->with(Task::class, 123)->willReturn($this->task); - $this->entityManager->expects($this->exactly(2))->method('flush'); - $this->connection->expects($this->once())->method('connect'); - $this->entityManager->expects($this->once())->method('getConnection')->willReturn($this->connection); - $this->bugHelper - ->expects($this->once()) - ->method('createBug') - ->with([$this->steps[0], $this->steps[1], $this->steps[2]], 'Can not run the third step') - ->willReturn($bug = new Bug()); + ->method('run') + ->with( + $this->steps, + $this->task, + $this->callback(function (callable $exceptionCallback) use ($exception) { + if ($exception) { + $exceptionCallback($exception, $this->steps); + } + return true; + }) + ); + if ($exception) { + $this->messageBus + ->expects($this->once()) + ->method('dispatch') + ->with($this->callback(function ($message) use ($exception) { + return $message instanceof CreateBugMessage && + $message->getMessage() === $exception->getMessage() && + $message->getSteps() === $this->steps && + 123 === $message->getTaskId(); + })) + ->willReturn(new Envelope(new \stdClass())); + } else { + $this->messageBus->expects($this->never())->method('dispatch'); + } $this->taskHelper->run(123); - $this->assertFalse($this->task->isRunning()); - $this->assertSame([$bug], $this->task->getBugs()->toArray()); } - public function testRunReachMaxSteps(): void + public function exceptionProvider(): array { - $this->config->expects($this->exactly(2))->method('getMaxSteps')->willReturn(2); - $generator = $this->createMock(GeneratorInterface::class); - $generator->expects($this->once())->method('generate')->with($this->task)->willReturnCallback( - fn () => yield from $this->steps - ); - $this->driver->expects($this->once())->method('quit'); - $this->config->expects($this->once())->method('getGenerator')->willReturn('random'); - $this->generatorManager->expects($this->once())->method('getGenerator')->with('random')->willReturn($generator); - $this->selenoidHelper - ->expects($this->once()) - ->method('getCapabilities') - ->with($this->task, $this->task->isDebug()) - ->willReturn($this->capabilities); - $this->selenoidHelper - ->expects($this->once()) - ->method('createDriver') - ->with($this->capabilities) - ->willReturn($this->driver); - $this->stepRunner->expects($this->exactly(2))->method('run') - ->with($this->isInstanceOf(StepInterface::class), $this->revision, $this->driver); - $this->entityManager->expects($this->once())->method('find')->with(Task::class, 123)->willReturn($this->task); - $this->entityManager->expects($this->exactly(2))->method('flush'); - $this->connection->expects($this->once())->method('connect'); - $this->entityManager->expects($this->once())->method('getConnection')->willReturn($this->connection); - $this->entityManager->expects($this->never())->method('persist'); - $this->taskHelper->run(123); - $this->assertFalse($this->task->isRunning()); + return [ + [null], + [new Exception('Something wrong')], + ]; } } diff --git a/tests/StepsTestCase.php b/tests/StepsTestCase.php deleted file mode 100644 index 7a236386..00000000 --- a/tests/StepsTestCase.php +++ /dev/null @@ -1,24 +0,0 @@ - $step) { - $this->assertInstanceOf(StepInterface::class, $step); - $this->assertStep($expected[$index], $step); - } - } - - public function assertStep(StepInterface $expected, StepInterface $actual): void - { - $this->assertSame($expected->getPlaces(), $actual->getPlaces()); - $this->assertSame($expected->getColor()->getValues(), $actual->getColor()->getValues()); - $this->assertSame($expected->getTransition(), $actual->getTransition()); - } -} diff --git a/tests/TienvxMbtBundleTest.php b/tests/TienvxMbtBundleTest.php index 453c4cae..30b5dfec 100644 --- a/tests/TienvxMbtBundleTest.php +++ b/tests/TienvxMbtBundleTest.php @@ -2,9 +2,17 @@ namespace Tienvx\Bundle\MbtBundle\Tests; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Tienvx\Bundle\MbtBundle\Command\CommandManager; use Tienvx\Bundle\MbtBundle\DependencyInjection\Compiler\PluginPass; +use Tienvx\Bundle\MbtBundle\Plugin\PluginInterface; +use Tienvx\Bundle\MbtBundle\Service\SelenoidHelper; use Tienvx\Bundle\MbtBundle\TienvxMbtBundle; /** @@ -12,13 +20,27 @@ */ class TienvxMbtBundleTest extends TestCase { + protected const CONFIG = [ + TienvxMbtBundle::WEBDRIVER_URI => 'http://localhost:4444', + TienvxMbtBundle::UPLOAD_DIR => '/path/to/var/uploads', + ]; + + protected ContainerBuilder $builder; + protected DefinitionConfigurator|MockObject $definition; + protected TienvxMbtBundle $bundle; + + protected function setUp(): void + { + $this->builder = new ContainerBuilder(); + $this->definition = $this->createMock(DefinitionConfigurator::class); + $this->bundle = new TienvxMbtBundle(); + } + public function testBuild(): void { - $container = new ContainerBuilder(); - $oldPasses = $container->getCompiler()->getPassConfig()->getBeforeOptimizationPasses(); - $bundle = new TienvxMbtBundle(); - $bundle->build($container); - $newPasses = $container->getCompiler()->getPassConfig()->getBeforeOptimizationPasses(); + $oldPasses = $this->builder->getCompiler()->getPassConfig()->getBeforeOptimizationPasses(); + $this->bundle->build($this->builder); + $newPasses = $this->builder->getCompiler()->getPassConfig()->getBeforeOptimizationPasses(); $this->assertSame(1, count($newPasses) - count($oldPasses)); $hasPass = false; foreach ($newPasses as $pass) { @@ -28,4 +50,32 @@ public function testBuild(): void } $this->assertTrue($hasPass); } + + public function testConfigure(): void + { + $this->definition->expects($this->once())->method('import')->with('../config/definition.php'); + $this->bundle->configure($this->definition); + } + + public function testLoadExtension(): void + { + $instanceof = []; + $container = new ContainerConfigurator( + $this->builder, + new PhpFileLoader($this->builder, new FileLocator(\dirname(__DIR__) . '/config')), + $instanceof, + '', + '' + ); + $this->bundle->loadExtension(static::CONFIG, $container, $this->builder); + $this->assertSame([ + ['setWebdriverUri', [static::CONFIG[TienvxMbtBundle::WEBDRIVER_URI]]], + ], $this->builder->findDefinition(SelenoidHelper::class)->getMethodCalls()); + $this->assertSame([ + ['setUploadDir', [static::CONFIG[TienvxMbtBundle::UPLOAD_DIR]]], + ], $this->builder->findDefinition(CommandManager::class)->getMethodCalls()); + $autoConfigured = $this->builder->getAutoconfiguredInstanceof()[PluginInterface::class]; + $this->assertTrue($autoConfigured->hasTag(PluginInterface::TAG)); + $this->assertTrue($autoConfigured->isLazy()); + } } diff --git a/tests/Validator/ValidCommandValidatorTest.php b/tests/Validator/ValidCommandValidatorTest.php index 7e2fd529..737d511c 100644 --- a/tests/Validator/ValidCommandValidatorTest.php +++ b/tests/Validator/ValidCommandValidatorTest.php @@ -2,20 +2,13 @@ namespace Tienvx\Bundle\MbtBundle\Tests\Validator; +use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -use Tienvx\Bundle\MbtBundle\Command\CommandPreprocessor; -use Tienvx\Bundle\MbtBundle\Command\CommandRunnerManager; -use Tienvx\Bundle\MbtBundle\Command\Runner\AlertCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\AssertionRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\KeyboardCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\MouseCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\ScriptCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\StoreCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\WaitCommandRunner; -use Tienvx\Bundle\MbtBundle\Command\Runner\WindowCommandRunner; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Tienvx\Bundle\MbtBundle\Command\CommandManager; use Tienvx\Bundle\MbtBundle\Validator\ValidCommand; use Tienvx\Bundle\MbtBundle\Validator\ValidCommandValidator; use Tienvx\Bundle\MbtBundle\ValueObject\Model\Command; @@ -23,35 +16,29 @@ /** * @covers \Tienvx\Bundle\MbtBundle\Validator\ValidCommandValidator * - * @uses \Tienvx\Bundle\MbtBundle\Command\CommandRunnerManager - * @uses \Tienvx\Bundle\MbtBundle\Command\CommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\AlertCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\AssertionRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\KeyboardCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\MouseCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\ScriptCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\StoreCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\WaitCommandRunner - * @uses \Tienvx\Bundle\MbtBundle\Command\Runner\WindowCommandRunner + * @uses \Tienvx\Bundle\MbtBundle\Command\CommandManager + * @uses \Tienvx\Bundle\MbtBundle\Command\AbstractCommand + * @uses \Tienvx\Bundle\MbtBundle\Command\Mouse\AbstractMousePointCommand + * @uses \Tienvx\Bundle\MbtBundle\Command\Mouse\ClickAtCommand + * @uses \Tienvx\Bundle\MbtBundle\Command\Mouse\ClickCommand + * @uses \Tienvx\Bundle\MbtBundle\Command\Window\AbstractWindowCommand + * @uses \Tienvx\Bundle\MbtBundle\Command\Window\OpenCommand + * @uses \Tienvx\Bundle\MbtBundle\Command\Mouse\DragAndDropToObjectCommand * @uses \Tienvx\Bundle\MbtBundle\Model\Model\Revision\Command */ class ValidCommandValidatorTest extends ConstraintValidatorTestCase { + protected HttpClientInterface|MockObject $httpClient; + + protected function setUp(): void + { + $this->httpClient = $this->createMock(HttpClientInterface::class); + parent::setUp(); + } + protected function createValidator() { - return new ValidCommandValidator(new CommandRunnerManager( - [ - new AlertCommandRunner(), - new AssertionRunner(), - new KeyboardCommandRunner(), - new MouseCommandRunner(), - new ScriptCommandRunner(), - new StoreCommandRunner(), - new WaitCommandRunner(), - new WindowCommandRunner(), - ], - new CommandPreprocessor() - )); + return new ValidCommandValidator(new CommandManager($this->httpClient)); } /** @@ -67,7 +54,7 @@ public function testValidCommand($command) public function validCommandsProvider(): array { $command = new Command(); - $command->setCommand(MouseCommandRunner::CLICK_AT); + $command->setCommand('clickAt'); $command->setTarget('css=.button'); $command->setValue('123,234'); @@ -80,7 +67,7 @@ public function validCommandsProvider(): array public function testInvalidCommand() { $constraint = new ValidCommand([ - 'commandMessage' => 'invalid command', + 'invalidCommandMessage' => 'invalid command', ]); $command = new Command(); @@ -101,7 +88,7 @@ public function testRequiredTarget() ]); $command = new Command(); - $command->setCommand(MouseCommandRunner::CLICK); + $command->setCommand('click'); $command->setTarget(null); $this->validator->validate($command, $constraint); @@ -118,7 +105,7 @@ public function testRequiredValue() ]); $command = new Command(); - $command->setCommand(MouseCommandRunner::CLICK_AT); + $command->setCommand('clickAt'); $command->setTarget('css=.button'); $command->setValue(null); $this->validator->validate($command, $constraint); @@ -136,7 +123,7 @@ public function testInvalidTarget() ]); $command = new Command(); - $command->setCommand(WindowCommandRunner::OPEN); + $command->setCommand('open'); $command->setTarget('testing'); $this->validator->validate($command, $constraint); @@ -146,6 +133,24 @@ public function testInvalidTarget() ->assertRaised(); } + public function testInvalidValue() + { + $constraint = new ValidCommand([ + 'valueInvalidMessage' => 'invalid value', + ]); + + $command = new Command(); + $command->setCommand('dragAndDropToObject'); + $command->setTarget('css=.from'); + $command->setValue('.to'); + $this->validator->validate($command, $constraint); + + $this->buildViolation('invalid value') + ->setCode(ValidCommand::IS_COMMAND_INVALID_ERROR) + ->atPath('property.path.value') + ->assertRaised(); + } + public function testUnexpectedType(): void { $this->expectException(UnexpectedTypeException::class); diff --git a/tests/ValueObject/Bug/StepTest.php b/tests/ValueObject/Bug/StepTest.php new file mode 100644 index 00000000..18cafbad --- /dev/null +++ b/tests/ValueObject/Bug/StepTest.php @@ -0,0 +1,20 @@ +places, $this->color, $this->transition); + } +} diff --git a/tests/ValueObject/Model/CommandTest.php b/tests/ValueObject/Model/CommandTest.php new file mode 100644 index 00000000..9d2460cc --- /dev/null +++ b/tests/ValueObject/Model/CommandTest.php @@ -0,0 +1,20 @@ + [ 'model' => [ + 'missing_places' => 'Places are missing.', 'places_invalid' => 'Places are invalid.', 'missing_start_transition' => 'Missing start transition.', 'too_many_start_transitions' => 'There must be only one start transition.', @@ -12,6 +13,7 @@ 'required_target' => 'The target is required.', 'invalid_target' => 'The target is not valid.', 'required_value' => 'The value is required.', + 'invalid_value' => 'The value is not valid.', ], ], 'bug' => [