Skip to content

Commit

Permalink
Merge pull request #4641 from neos/task/3594-behat-based-fusion-tests
Browse files Browse the repository at this point in the history
TASK: Behat-based Fusion tests (v1)
  • Loading branch information
mhsdesign authored Oct 30, 2023
2 parents dc4e77b + cade98c commit 9ab98a4
Show file tree
Hide file tree
Showing 7 changed files with 1,003 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,6 @@ public function iHaveTheFollowingNodes($table)
$identifier = null;
}

if (isset($row['Hidden']) && $row['Hidden'] === 'true') {
$hidden = true;
} else {
$hidden = false;
}

$parentNode = $context->getNode($parentPath);
if ($parentNode === null) {
throw new \Exception(sprintf('Could not get parent node with path %s to create node %s', $parentPath, $path));
Expand All @@ -133,8 +127,12 @@ public function iHaveTheFollowingNodes($table)
$node->setProperty($propertyName, $propertyValue);
}
}

$node->setHidden($hidden);
if (isset($row['Hidden']) && $row['Hidden'] === 'true') {
$node->setHidden(true);
}
if (isset($row['Hidden in index']) && $row['Hidden in index'] === 'true') {
$node->setHiddenInIndex(true);
}
}

// Make sure we do not use cached instances
Expand Down
29 changes: 23 additions & 6 deletions Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,21 @@
use Neos\ContentRepository\Tests\Behavior\Features\Bootstrap\NodeAuthorizationTrait;
use Neos\ContentRepository\Tests\Behavior\Features\Bootstrap\NodeOperationsTrait;
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Flow\Security\AccountRepository;
use Neos\Flow\ResourceManagement\ResourceManager;
use Neos\Flow\Tests\Behavior\Features\Bootstrap\IsolatedBehatStepsTrait;
use Neos\Flow\Tests\Behavior\Features\Bootstrap\SecurityOperationsTrait;
use Neos\Flow\Utility\Environment;
use Neos\Media\Domain\Model\Image;
use Neos\Media\Domain\Repository\AssetRepository;
use Neos\Neos\Domain\Model\Site;
use Neos\Neos\Domain\Repository\SiteRepository;
use Neos\Neos\Domain\Service\SiteExportService;
use Neos\Neos\Domain\Service\SiteImportService;
use Neos\Neos\Domain\Service\SiteService;
use Neos\Neos\Domain\Service\UserService;
use Neos\Neos\Service\PublishingService;
use Neos\Neos\Tests\Behavior\Features\Bootstrap\FusionTrait;
use Neos\Neos\Tests\Functional\Command\BehatTestHelper;
use Neos\Party\Domain\Repository\PartyRepository;
use Neos\Utility\Arrays;
use Neos\Utility\Files;
use Neos\Utility\ObjectAccess;
Expand All @@ -47,6 +49,7 @@
require_once(__DIR__ . '/../../../../../Neos.ContentRepository/Tests/Behavior/Features/Bootstrap/NodeOperationsTrait.php');
require_once(__DIR__ . '/../../../../../Neos.ContentRepository/Tests/Behavior/Features/Bootstrap/NodeAuthorizationTrait.php');
require_once(__DIR__ . '/HistoryDefinitionsTrait.php');
require_once(__DIR__ . '/FusionTrait.php');

/**
* Features context
Expand All @@ -59,6 +62,7 @@ class FeatureContext extends MinkContext
use SecurityOperationsTrait;
use IsolatedBehatStepsTrait;
use HistoryDefinitionsTrait;
use FusionTrait;

/**
* @var string
Expand Down Expand Up @@ -130,10 +134,6 @@ public function theFollowingUsersExist(TableNode $table)
$rows = $table->getHash();
/** @var UserService $userService */
$userService = $this->objectManager->get(UserService::class);
/** @var PartyRepository $partyRepository */
$partyRepository = $this->objectManager->get(PartyRepository::class);
/** @var AccountRepository $accountRepository */
$accountRepository = $this->objectManager->get(AccountRepository::class);
foreach ($rows as $row) {
$roleIdentifiers = array_map(function ($role) {
return 'Neos.Neos:' . $role;
Expand All @@ -143,6 +143,22 @@ public function theFollowingUsersExist(TableNode $table)
$this->persistAll();
}

/**
* @Given an asset exists with id :assetId
*/
public function anAssetExistsWithId(string $assetId): void
{
/** @var ResourceManager $resourceManager */
$resourceManager = $this->objectManager->get(ResourceManager::class);
$resourceContent = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 111.42 125"><defs><style>.cls-1{fill:#010101;}</style></defs><title>neos_avatar_monochrome</title><g id="Ebene_2" data-name="Ebene 2"><g id="Layer_1" data-name="Layer 1"><path class="cls-1" d="M94.59,125H71.37L44.95,87.27v22.24L23.85,125H0V7.28L9.88,0H36.26L66.47,43V15.49L87.57,0h23.85V112.67ZM2.63,8.61V121.09l17.58-12.91V47.35l52.61,75.12h20.7l12.78-9.32H87.19L10,3.17ZM22.78,122.53l19.54-14.35V83.51L22.84,55.59v53.94l-17.72,13ZM12.85,2.63,88.68,110.68h20.11V2.63H89.32V80.16L34.89,2.63ZM69.11,46.79l17.58,25.1v-68L69.11,16.82Z"/></g></g></svg>';
$resource = $resourceManager->importResourceFromContent($resourceContent, 'test.svg');
$asset = new Image($resource);
ObjectAccess::setProperty($asset, 'Persistence_Object_Identifier', $assetId, true);

$this->objectManager->get(AssetRepository::class)->add($asset);
$this->persistAll();
}

/**
* @Given /^I am authenticated with "([^"]*)" and "([^"]*)" for the backend$/
*/
Expand Down Expand Up @@ -472,6 +488,7 @@ public function iHaveTheSite($siteName)
{
$site = new Site($siteName);
$site->setSiteResourcesPackageKey('Neos.Demo');
$site->setState(Site::STATE_ONLINE);
/** @var SiteRepository $siteRepository */
$siteRepository = $this->objectManager->get(SiteRepository::class);
$siteRepository->add($site);
Expand Down
184 changes: 184 additions & 0 deletions Neos.Neos/Tests/Behavior/Features/Bootstrap/FusionTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

namespace Neos\Neos\Tests\Behavior\Features\Bootstrap;

use Behat\Gherkin\Node\PyStringNode;
use Neos\ContentRepository\Tests\Behavior\Features\Bootstrap\NodeOperationsTrait;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\ActionResponse;
use Neos\Flow\Mvc\Controller\Arguments;
use Neos\Flow\Mvc\Controller\ControllerContext;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Neos\Flow\Tests\FunctionalTestRequestHandler;
use Neos\Flow\Tests\Unit\Http\Fixtures\SpyRequestHandler;
use Neos\Fusion\Core\FusionSourceCodeCollection;
use Neos\Fusion\Core\Parser;
use Neos\Fusion\Core\RuntimeFactory;
use Neos\Neos\Domain\Service\ContentContext;
use Neos\Neos\Routing\RequestUriHostMiddleware;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* @internal only for behat tests within the Neos.Neos package
*/
trait FusionTrait
{
use NodeOperationsTrait;

private ?ActionRequest $fusionRequest = null;
private array $fusionContext = [];

private ?string $renderingResult = null;

private ?string $fusionCode = null;

private ?\Throwable $lastRenderingException = null;

/**
* @BeforeScenario
*/
public function setupFusionContext(): void
{
$this->fusionRequest = null;
$this->fusionContext = [];
$this->fusionCode = null;
$this->renderingResult = null;
}

/**
* @When the Fusion context node is :nodeIdentifier
*/
public function theFusionContextNodeIs(string $nodeIdentifier): void
{
/** @var ContentContext $context */
$context = $this->getContextForProperties([]);
$this->fusionContext['node'] = $context->getNodeByIdentifier($nodeIdentifier);
if ($this->fusionContext['node'] === null) {
throw new \InvalidArgumentException(sprintf('Node with identifier "%s" could not be found in the "%s" workspace', $nodeIdentifier, $context->getWorkspaceName()), 1696700222);
}
$flowQuery = new FlowQuery([$this->fusionContext['node']]);
$this->fusionContext['documentNode'] = $flowQuery->closest('[instanceof Neos.Neos:Document]')->get(0);
if ($this->fusionContext['documentNode'] === null) {
throw new \RuntimeException(sprintf('Failed to find closest document node for node with identifier "%s"', $nodeIdentifier), 1697790940);
}
$this->fusionContext['site'] = $context->getCurrentSiteNode();
if ($this->fusionContext['site'] === null) {
throw new \RuntimeException(sprintf('Failed to resolve site node for node with identifier "%s"', $nodeIdentifier), 1697790963);
}
}

/**
* @When the Fusion context request URI is :requestUri
*/
public function theFusionContextRequestIs(string $requestUri = null): void
{
$httpRequest = $this->objectManager->get(ServerRequestFactoryInterface::class)->createServerRequest('GET', $requestUri);
$httpRequest = $this->addRoutingParameters($httpRequest);

$this->fusionRequest = ActionRequest::fromHttpRequest($httpRequest);
}

private function addRoutingParameters(ServerRequestInterface $httpRequest): ServerRequestInterface
{
$spyMiddleware = new SpyRequestHandler();
(new RequestUriHostMiddleware())->process($httpRequest, $spyMiddleware);
return $spyMiddleware->getHandledRequest();
}

/**
* @When I have the following Fusion setup:
*/
public function iHaveTheFollowingFusionSetup(PyStringNode $fusionCode): void
{
$this->fusionCode = $fusionCode->getRaw();
}

/**
* @When I execute the following Fusion code:
* @When I execute the following Fusion code on path :path:
*/
public function iExecuteTheFollowingFusionCode(PyStringNode $fusionCode, string $path = 'test'): void
{
if ($this->fusionRequest === null) {
$this->theFusionContextRequestIs('http://localhost');
}
$requestHandler = new FunctionalTestRequestHandler(self::$bootstrap);
$requestHandler->setHttpRequest($this->fusionRequest->getHttpRequest());
self::$bootstrap->setActiveRequestHandler($requestHandler);
$this->throwExceptionIfLastRenderingLedToAnError();
$this->renderingResult = null;
$fusionAst = (new Parser())->parseFromSource(FusionSourceCodeCollection::fromString($this->fusionCode . chr(10) . $fusionCode->getRaw()));
$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($this->fusionRequest);
$controllerContext = new ControllerContext($this->fusionRequest, new ActionResponse(), new Arguments(), $uriBuilder);

$fusionRuntime = (new RuntimeFactory())->createFromConfiguration($fusionAst, $controllerContext);
$fusionRuntime->pushContextArray($this->fusionContext);
try {
$this->renderingResult = $fusionRuntime->render($path);
} catch (\Throwable $exception) {
$this->lastRenderingException = $exception;
}
$fusionRuntime->popContext();
}

/**
* @Then I expect the following Fusion rendering result:
*/
public function iExpectTheFollowingFusionRenderingResult(PyStringNode $expectedResult): void
{
Assert::assertSame($expectedResult->getRaw(), $this->renderingResult);
}

/**
* @Then I expect the following Fusion rendering result as HTML:
*/
public function iExpectTheFollowingFusionRenderingResultAsHtml(PyStringNode $expectedResult): void
{
Assert::assertIsString($this->renderingResult, 'Previous Fusion rendering did not produce a string');
$stripWhitespace = static fn (string $input): string => preg_replace(['/>[^\S ]+/s', '/[^\S ]+</s', '/(\s)+/s', '/> </s'], ['>', '<', '\\1', '><'], $input);

$expectedDom = new \DomDocument();
$expectedDom->preserveWhiteSpace = false;
$expectedDom->loadHTML($stripWhitespace($expectedResult->getRaw()));

$actualDom = new \DomDocument();
$actualDom->preserveWhiteSpace = false;
$actualDom->loadHTML($stripWhitespace($this->renderingResult));

Assert::assertSame($expectedDom->saveHTML(), $actualDom->saveHTML());
}
/**
* @Then I expect the following Fusion rendering error:
*/
public function iExpectTheFollowingFusionRenderingError(PyStringNode $expectedError): void
{
Assert::assertNotNull($this->lastRenderingException, 'The previous rendering did not lead to an error');
Assert::assertSame($expectedError->getRaw(), $this->lastRenderingException->getMessage());
$this->lastRenderingException = null;
}

/**
* @AfterScenario
*/
public function throwExceptionIfLastRenderingLedToAnError(): void
{
if ($this->lastRenderingException !== null) {
throw new \RuntimeException(sprintf('The last rendering led to an error: %s', $this->lastRenderingException->getMessage()), 1698319254, $this->lastRenderingException);
}
}
}
60 changes: 60 additions & 0 deletions Neos.Neos/Tests/Behavior/Features/Fusion/ContentCase.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@fixtures
Feature: Tests for the "Neos.Neos:ContentCase" Fusion prototype

Background:
Given I have the site "a"
And I have the following NodeTypes configuration:
"""yaml
'unstructured': {}
'Neos.Neos:FallbackNode': {}
'Neos.Neos:Document': {}
'Neos.Neos:Test.DocumentType1':
superTypes:
'Neos.Neos:Document': true
'Neos.Neos:Test.DocumentType2':
superTypes:
'Neos.Neos:Document': true
"""
And I have the following nodes:
| Identifier | Path | Node Type |
| root | /sites | unstructured |
| a | /sites/a | Neos.Neos:Test.DocumentType1 |
| a1 | /sites/a/a1 | Neos.Neos:Test.DocumentType2 |
And the Fusion context node is "a1"
And the Fusion context request URI is "http://localhost"

Scenario: ContentCase without corresponding implementation
When I execute the following Fusion code:
"""fusion
include: resource://Neos.Fusion/Private/Fusion/Root.fusion
include: resource://Neos.Neos/Private/Fusion/Root.fusion
test = Neos.Neos:ContentCase
"""
Then I expect the following Fusion rendering error:
"""
The Fusion object "Neos.Neos:Test.DocumentType2" cannot be rendered:
Most likely you mistyped the prototype name or did not define
the Fusion prototype with "prototype(Neos.Neos:Test.DocumentType2) < prototype(...)".
Other possible reasons are a missing parent-prototype or
a missing "@class" annotation for prototypes without parent.
It is also possible your Fusion file is not read because
of a missing "include:" statement.
"""

Scenario: ContentCase with corresponding implementation
When I execute the following Fusion code:
"""fusion
include: resource://Neos.Fusion/Private/Fusion/Root.fusion
include: resource://Neos.Neos/Private/Fusion/Root.fusion
prototype(Neos.Neos:Test.DocumentType2) < prototype(Neos.Fusion:Value) {
value = 'implementation for DocumentType2'
}
test = Neos.Neos:ContentCase
"""
Then I expect the following Fusion rendering result:
"""
implementation for DocumentType2
"""
Loading

0 comments on commit 9ab98a4

Please sign in to comment.