diff --git a/composer.json b/composer.json index 2c3c87c..4aa391e 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "behat/mink": "^1.7", "behat/mink-browserkit-driver": "^1.3", "behat/mink-extension": "^2.2", + "friends-of-behat/service-container-extension": "^1.0", "phpstan/phpstan-shim": "^0.11", "sylius-labs/coding-standard": "^3.0", "symfony/framework-bundle": "^3.4|^4.2", diff --git a/features/mink_integration/mink_integration_with_dependency_injection.feature b/features/mink_integration/mink_integration.feature similarity index 98% rename from features/mink_integration/mink_integration_with_dependency_injection.feature rename to features/mink_integration/mink_integration.feature index 3318d94..fbe9290 100644 --- a/features/mink_integration/mink_integration_with_dependency_injection.feature +++ b/features/mink_integration/mink_integration.feature @@ -1,4 +1,4 @@ -Feature: Mink integration with dependency injection +Feature: Mink integration Background: Given a working Symfony application with SymfonyExtension configured diff --git a/features/mink_integration/mink_integration_with_context_initializer.feature b/features/mink_integration/mink_integration_with_context_initializer.feature deleted file mode 100644 index e2264c6..0000000 --- a/features/mink_integration/mink_integration_with_context_initializer.feature +++ /dev/null @@ -1,78 +0,0 @@ -Feature: Mink integration with context initializer - - Scenario: Passing Mink instance and parameters through context initializer - Given a working Symfony application with SymfonyExtension configured - And a Behat configuration containing: - """ - default: - extensions: - Behat\MinkExtension: - base_url: "http://localhost:8080/" - default_session: symfony - sessions: - symfony: - symfony: ~ - suites: - default: - contexts: - - App\Tests\SomeContext - """ - And a feature file containing: - """ - Feature: - Scenario: - When I visit the page "/hello-world" - Then I should see "Hello world!" on the page - And the base url from Mink parameters should be "http://localhost:8080/" - - # Doubling the scenario to account for some weird error connected to Mink's session - Scenario: - When I visit the page "/hello-world" - Then I should see "Hello world!" on the page - And the base url from Mink parameters should be "http://localhost:8080/" - """ - And a context file "tests/SomeContext.php" containing: - """ - mink = $mink; - } - - public function setMinkParameters(array $minkParameters): void - { - $this->parameters = $minkParameters; - } - - /** @When I visit the page :page */ - public function visitPage(string $page): void - { - $this->mink->getSession()->visit($page); - } - - /** @Then I should see :content on the page */ - public function shouldSeeContentOnPage(string $content): void - { - assert(false !== strpos($this->mink->getSession()->getPage()->getContent(), $content)); - } - - /** @Then the base url from Mink parameters should be :expected */ - public function baseUrlShouldBe(string $expected): void - { - assert(isset($this->parameters['base_url'])); - assert($this->parameters['base_url'] === $expected); - } - } - """ - When I run Behat - Then it should pass diff --git a/features/sanity_checks/class_resolvers_compatibility.feature b/features/sanity_checks/class_resolvers_compatibility.feature new file mode 100644 index 0000000..761c8b8 --- /dev/null +++ b/features/sanity_checks/class_resolvers_compatibility.feature @@ -0,0 +1,65 @@ +Feature: Class resolvers compatibility + + Scenario: Using class resolvers while handling context environment + Given a working Symfony application with SymfonyExtension configured + And a Behat configuration containing: + """ + default: + extensions: + FriendsOfBehat\ServiceContainerExtension: + imports: + - "tests/class_resolver.yml" + + suites: + default: + contexts: + - class:resolved:context + """ + And a Behat services definition file "tests/class_resolver.yml" containing: + """ + services: + App\Tests\CustomClassResolver: + tags: ["context.class_resolver"] + """ + And a Behat service implementation file "tests/CustomClassResolver.php" containing: + """ + foo = $foo; + } + + /** @Then it should pass */ + public function itShouldPass(): void + { + assert($this->foo instanceof Foo); + } + } + """ + When I run Behat + Then it should pass diff --git a/features/sanity_checks/context_initializer_compatibility.feature b/features/sanity_checks/context_initializer_compatibility.feature new file mode 100644 index 0000000..df545f2 --- /dev/null +++ b/features/sanity_checks/context_initializer_compatibility.feature @@ -0,0 +1,71 @@ +Feature: Context initializer compatibility + + Scenario: Using class resolvers while handling context environment + Given a working Symfony application with SymfonyExtension configured + And a Behat configuration containing: + """ + default: + extensions: + FriendsOfBehat\ServiceContainerExtension: + imports: + - "tests/context_initializer.yml" + + suites: + default: + contexts: + - App\Tests\SomeContext + """ + And a Behat services definition file "tests/context_initializer.yml" containing: + """ + services: + App\Tests\CustomContextInitializer: + tags: ["context.initializer"] + """ + And a Behat service implementation file "tests/CustomContextInitializer.php" containing: + """ + makeItPass(true); + } + } + """ + And a feature file containing: + """ + Feature: + Scenario: + Then it should pass + """ + And a context file "tests/SomeContext.php" containing: + """ + shouldPass = $shouldPass; + } + + /** @Then it should pass */ + public function itShouldPass(): void + { + assert($this->shouldPass === true); + } + } + """ + When I run Behat + Then it should pass diff --git a/phpstan.neon b/phpstan.neon index 5894523..64c7cba 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,5 +5,5 @@ parameters: - '/Cannot access offset 0 on callable/' - '/Cannot access offset 1 on callable/' - '/Cannot call method [a-zA-Z0-9]+\(\) on Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface|null\./' - - '/Method FriendsOfBehat\\SymfonyExtension\\Context\\Environment\\InitialisedContextServiceEnvironment::bindCallee\(\) should return callable/' + - '/Method FriendsOfBehat\\SymfonyExtension\\Context\\Environment\\InitializedSymfonyExtensionEnvironment::bindCallee\(\) should return callable/' - '/Strict comparison using === between 0\|1 and 2 will always evaluate to false\./' diff --git a/src/Context/Environment/Handler/ContextServiceEnvironmentHandler.php b/src/Context/Environment/Handler/ContextServiceEnvironmentHandler.php index 8792d85..dca3f34 100644 --- a/src/Context/Environment/Handler/ContextServiceEnvironmentHandler.php +++ b/src/Context/Environment/Handler/ContextServiceEnvironmentHandler.php @@ -14,15 +14,17 @@ declare(strict_types=1); namespace FriendsOfBehat\SymfonyExtension\Context\Environment\Handler; use Behat\Behat\Context\Context; -use Behat\Behat\Context\Initializer\ContextInitializer; +use Behat\Behat\Context\Environment\ContextEnvironment; +use Behat\Behat\Context\Environment\InitializedContextEnvironment; use Behat\Testwork\Environment\Environment; use Behat\Testwork\Environment\Exception\EnvironmentIsolationException; use Behat\Testwork\Environment\Handler\EnvironmentHandler; use Behat\Testwork\Suite\Exception\SuiteConfigurationException; +use Behat\Testwork\Suite\GenericSuite; use Behat\Testwork\Suite\Suite; use FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle; -use FriendsOfBehat\SymfonyExtension\Context\Environment\InitialisedContextServiceEnvironment; -use FriendsOfBehat\SymfonyExtension\Context\Environment\UninitialisedContextServiceEnvironment; +use FriendsOfBehat\SymfonyExtension\Context\Environment\InitializedSymfonyExtensionEnvironment; +use FriendsOfBehat\SymfonyExtension\Context\Environment\UninitializedSymfonyExtensionEnvironment; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\KernelInterface; @@ -31,12 +33,13 @@ final class ContextServiceEnvironmentHandler implements EnvironmentHandler /** @var KernelInterface */ private $symfonyKernel; - /** @var ContextInitializer[] */ - private $contextInitializers = []; + /** @var EnvironmentHandler */ + private $decoratedEnvironmentHandler; - public function __construct(KernelInterface $symfonyKernel) + public function __construct(KernelInterface $symfonyKernel, EnvironmentHandler $decoratedEnvironmentHandler) { $this->symfonyKernel = $symfonyKernel; + $this->decoratedEnvironmentHandler = $decoratedEnvironmentHandler; } public function supportsSuite(Suite $suite): bool @@ -46,21 +49,31 @@ final class ContextServiceEnvironmentHandler implements EnvironmentHandler public function buildEnvironment(Suite $suite): Environment { - $environment = new UninitialisedContextServiceEnvironment($suite); - foreach ($this->getSuiteContextsServices($suite) as $contextId) { - $environment->registerContextService($contextId, $this->getContextClass($contextId)); + $symfonyContexts = []; + + foreach ($this->getSuiteContextsServices($suite) as $serviceId) { + if (!$this->getContainer()->has($serviceId)) { + continue; + } + + $symfonyContexts[$serviceId] = get_class($this->getContainer()->get($serviceId)); } - return $environment; + $delegatedSuite = $this->cloneSuiteWithoutContexts($suite, array_keys($symfonyContexts)); + + /** @var ContextEnvironment $delegatedEnvironment */ + $delegatedEnvironment = $this->decoratedEnvironmentHandler->buildEnvironment($delegatedSuite); + + return new UninitializedSymfonyExtensionEnvironment($suite, $symfonyContexts, $delegatedEnvironment); } public function supportsEnvironmentAndSubject(Environment $environment, $testSubject = null): bool { - return $environment instanceof UninitialisedContextServiceEnvironment; + return $environment instanceof UninitializedSymfonyExtensionEnvironment; } /** - * @param UninitialisedContextServiceEnvironment $uninitializedEnvironment + * @param UninitializedSymfonyExtensionEnvironment $uninitializedEnvironment * * @throws EnvironmentIsolationException */ @@ -68,22 +81,25 @@ final class ContextServiceEnvironmentHandler implements EnvironmentHandler { $this->assertEnvironmentCanBeIsolated($uninitializedEnvironment, $testSubject); - $environment = new InitialisedContextServiceEnvironment($uninitializedEnvironment->getSuite()); - foreach ($uninitializedEnvironment->getContextServices() as $contextId) { + $environment = new InitializedSymfonyExtensionEnvironment($uninitializedEnvironment->getSuite()); + + foreach ($uninitializedEnvironment->getServices() as $serviceId) { /** @var Context $context */ - $context = $this->getContext($contextId); - $this->initializeInstance($context); + $context = $this->getContainer()->get($serviceId); + + $environment->registerContext($context); + } + + /** @var InitializedContextEnvironment $delegatedEnvironment */ + $delegatedEnvironment = $this->decoratedEnvironmentHandler->isolateEnvironment($uninitializedEnvironment->getDelegatedEnvironment()); + + foreach ($delegatedEnvironment->getContexts() as $context) { $environment->registerContext($context); } return $environment; } - public function registerContextInitializer(ContextInitializer $initializer): void - { - $this->contextInitializers[] = $initializer; - } - /** * @return string[] * @@ -91,17 +107,49 @@ final class ContextServiceEnvironmentHandler implements EnvironmentHandler */ private function getSuiteContextsServices(Suite $suite): array { - $contextsServices = $suite->getSetting('contexts'); + $contexts = $suite->getSetting('contexts'); - if (!is_array($contextsServices)) { + if (!is_array($contexts)) { throw new SuiteConfigurationException(sprintf( '"contexts" setting of the "%s" suite is expected to be an array, %s given.', $suite->getName(), - gettype($contextsServices) + gettype($contexts) ), $suite->getName()); } - return $contextsServices; + return array_map([$this, 'normalizeContext'], $contexts); + } + + private function cloneSuiteWithoutContexts(Suite $suite, array $contextsToRemove): Suite + { + $contexts = $suite->getSetting('contexts'); + + if (!is_array($contexts)) { + throw new SuiteConfigurationException(sprintf( + '"contexts" setting of the "%s" suite is expected to be an array, %s given.', + $suite->getName(), + gettype($contexts) + ), $suite->getName()); + } + + $contexts = array_filter($contexts, function ($context) use ($contextsToRemove): bool { + return !in_array($this->normalizeContext($context), $contextsToRemove, true); + }); + + return new GenericSuite($suite->getName(), array_merge($suite->getSettings(), ['contexts' => $contexts])); + } + + private function normalizeContext($context): string + { + if (is_array($context)) { + return current(array_keys($context)); + } + + if (is_string($context)) { + return $context; + } + + throw new \Exception(); } /** @@ -118,52 +166,6 @@ final class ContextServiceEnvironmentHandler implements EnvironmentHandler } } - private function initializeInstance(Context $context): void - { - foreach ($this->contextInitializers as $initializer) { - $initializer->initializeContext($context); - } - } - - private function getContextClass(string $contextId): string - { - if ($this->getContainer()->has($contextId)) { - return get_class($this->getContainer()->get($contextId)); - } - - $class = '\\' . ltrim($contextId, '\\'); - - if (class_exists($class)) { - return $class; - } - - throw new \DomainException(sprintf('There is no service or class "%s".', $contextId)); - } - - private function getContext(string $contextId): Context - { - $class = '\\' . ltrim($contextId, '\\'); - - if ($this->getContainer()->has($contextId)) { - $context = $this->getContainer()->get($contextId); - } elseif (class_exists($class)) { - $context = new $class(); - } else { - throw new \DomainException(sprintf('There is no service or class "%s".', $contextId)); - } - - if (!$context instanceof Context) { - throw new \DomainException(sprintf( - 'Context "%s" referenced as "%s" needs to implement "%s".', - get_class($context), - $contextId, - Context::class - )); - } - - return $context; - } - private function getContainer(): ContainerInterface { try { diff --git a/src/Context/Environment/InitialisedContextServiceEnvironment.php b/src/Context/Environment/InitializedSymfonyExtensionEnvironment.php similarity index 95% rename from src/Context/Environment/InitialisedContextServiceEnvironment.php rename to src/Context/Environment/InitializedSymfonyExtensionEnvironment.php index d8c1904..b07da6e 100644 --- a/src/Context/Environment/InitialisedContextServiceEnvironment.php +++ b/src/Context/Environment/InitializedSymfonyExtensionEnvironment.php @@ -22,7 +22,7 @@ use FriendsOfBehat\SymfonyExtension\Context\Environment\Handler\ContextServiceEn /** * @see ContextServiceEnvironmentHandler */ -final class InitialisedContextServiceEnvironment implements ContextServiceEnvironment +final class InitializedSymfonyExtensionEnvironment implements SymfonyExtensionEnvironment { /** @var Suite */ private $suite; diff --git a/src/Context/Environment/ContextServiceEnvironment.php b/src/Context/Environment/SymfonyExtensionEnvironment.php similarity index 89% rename from src/Context/Environment/ContextServiceEnvironment.php rename to src/Context/Environment/SymfonyExtensionEnvironment.php index ebf7b95..b75b232 100644 --- a/src/Context/Environment/ContextServiceEnvironment.php +++ b/src/Context/Environment/SymfonyExtensionEnvironment.php @@ -19,6 +19,6 @@ use FriendsOfBehat\SymfonyExtension\Context\Environment\Handler\ContextServiceEn /** * @see ContextServiceEnvironmentHandler */ -interface ContextServiceEnvironment extends ContextEnvironment +interface SymfonyExtensionEnvironment extends ContextEnvironment { } diff --git a/src/Context/Environment/UninitialisedContextServiceEnvironment.php b/src/Context/Environment/UninitialisedContextServiceEnvironment.php deleted file mode 100644 index bbd6211..0000000 --- a/src/Context/Environment/UninitialisedContextServiceEnvironment.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace FriendsOfBehat\SymfonyExtension\Context\Environment; - -use Behat\Testwork\Environment\StaticEnvironment; -use FriendsOfBehat\SymfonyExtension\Context\Environment\Handler\ContextServiceEnvironmentHandler; - -/** - * @see ContextServiceEnvironmentHandler - */ -final class UninitialisedContextServiceEnvironment extends StaticEnvironment implements ContextServiceEnvironment -{ - /** @var string[] */ - private $contextServices = []; - - public function registerContextService(string $serviceId, string $serviceClass): void - { - $this->contextServices[$serviceId] = $serviceClass; - } - - public function getContextServices(): array - { - return array_keys($this->contextServices); - } - - public function hasContexts(): bool - { - return count($this->contextServices) > 0; - } - - public function getContextClasses(): array - { - return array_values($this->contextServices); - } - - public function hasContextClass($class): bool - { - return in_array($class, $this->contextServices, true); - } -} diff --git a/src/Context/Environment/UninitializedSymfonyExtensionEnvironment.php b/src/Context/Environment/UninitializedSymfonyExtensionEnvironment.php new file mode 100644 index 0000000..f3d0626 --- /dev/null +++ b/src/Context/Environment/UninitializedSymfonyExtensionEnvironment.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FriendsOfBehat\SymfonyExtension\Context\Environment; + +use Behat\Behat\Context\Environment\ContextEnvironment; +use Behat\Testwork\Environment\StaticEnvironment; +use Behat\Testwork\Suite\Suite; +use FriendsOfBehat\SymfonyExtension\Context\Environment\Handler\ContextServiceEnvironmentHandler; + +/** + * @see ContextServiceEnvironmentHandler + */ +final class UninitializedSymfonyExtensionEnvironment extends StaticEnvironment implements SymfonyExtensionEnvironment +{ + /** @var string[] */ + private $contexts; + + /** @var ContextEnvironment|null */ + private $delegatedEnvironment; + + public function __construct(Suite $suite, array $contexts, ContextEnvironment $delegatedEnvironment) + { + parent::__construct($suite); + + $this->contexts = $contexts; + $this->delegatedEnvironment = $delegatedEnvironment; + } + + public function getServices(): array + { + return array_keys($this->contexts); + } + + public function hasContexts(): bool + { + return count($this->contexts) > 0 || $this->delegatedEnvironment->hasContexts(); + } + + public function getContextClasses(): array + { + return array_merge(array_values($this->contexts), $this->delegatedEnvironment->getContextClasses()); + } + + public function hasContextClass($class): bool + { + return in_array($class, $this->contexts, true) || $this->delegatedEnvironment->hasContextClass($class); + } + + public function getDelegatedEnvironment(): ContextEnvironment + { + return $this->delegatedEnvironment; + } +} diff --git a/src/ServiceContainer/SymfonyExtension.php b/src/ServiceContainer/SymfonyExtension.php index fe20b2e..fdcafd5 100644 --- a/src/ServiceContainer/SymfonyExtension.php +++ b/src/ServiceContainer/SymfonyExtension.php @@ -88,7 +88,6 @@ final class SymfonyExtension implements Extension public function process(ContainerBuilder $container): void { - $this->processEnvironmentHandler($container); } private function registerMinkDriver(ExtensionManager $extensionManager): void @@ -137,6 +136,7 @@ final class SymfonyExtension implements Extension { $definition = new Definition(ContextServiceEnvironmentHandler::class, [ new Reference(self::KERNEL_ID), + new Reference('environment.handler.context'), ]); $definition->addTag(EnvironmentExtension::HANDLER_TAG, ['priority' => 128]); @@ -245,13 +245,4 @@ final class SymfonyExtension implements Extension return is_string($bootstrap) ? $bootstrap : null; } - - private function processEnvironmentHandler(ContainerBuilder $container): void - { - $definition = $container->findDefinition('fob_symfony.environment_handler.context_service'); - - foreach ($container->findTaggedServiceIds(ContextExtension::INITIALIZER_TAG) as $serviceId => $tags) { - $definition->addMethodCall('registerContextInitializer', [$container->getDefinition($serviceId)]); - } - } }