Skip to content

Commit

Permalink
TASK: Behat-based Fusion tests (v1)
Browse files Browse the repository at this point in the history
Related: #3594
  • Loading branch information
bwaidelich committed Oct 29, 2023
1 parent f271bd3 commit 4f9efcf
Show file tree
Hide file tree
Showing 6 changed files with 1,152 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class FeatureContext implements BehatContext
use CRBehavioralTestsSubjectProvider;
use RoutingTrait;
use MigrationsTrait;
use FusionTrait;

protected Environment $environment;

Expand Down
186 changes: 186 additions & 0 deletions Neos.Neos/Tests/Behavior/Features/Bootstrap/FusionTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?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.
*/

use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\ProjectedNodeTrait;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Tests\FunctionalTestRequestHandler;
use Neos\Fusion\Core\ExceptionHandlers\AbstractRenderingExceptionHandler;
use Neos\Fusion\Core\ExceptionHandlers\ThrowingHandler;
use Neos\Fusion\Core\FusionGlobals;
use Neos\Fusion\Core\FusionSourceCodeCollection;
use Neos\Fusion\Core\Parser;
use Neos\Fusion\Core\RuntimeFactory;
use Neos\Neos\Domain\Model\RenderingMode;
use Neos\Neos\Domain\Service\NodeTypeNameFactory;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ServerRequestFactoryInterface;

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

use RoutingTrait;
use ProjectedNodeTrait;
use CRTestSuiteRuntimeVariables;

private array $fusionGlobalContext = [];
private array $fusionContext = [];

private ?string $renderingResult = null;

private ?string $fusionCode = null;

private ?\Throwable $lastRenderingException = null;

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

/**
* @When I am in Fusion rendering mode:
*/
public function iAmInFusionRenderingMode(TableNode $renderingModeData): void
{
$data = $renderingModeData->getHash()[0];
$this->fusionGlobalContext['renderingMode'] = new RenderingMode($data['name'] ?? 'Testing', strtolower($data['isEdit'] ?? 'false') === 'true', strtolower($data['isPreview'] ?? 'false') === 'true', $data['title'] ?? 'Testing', $data['fusionPath'] ?? 'root', []);
}

/**
* @When the Fusion context node is :nodeAggregateId
*/
public function theFusionContextNodeIs(string $nodeAggregateId): void
{
$subgraph = $this->getCurrentSubgraph();
$this->fusionContext['node'] = $subgraph->findNodeById(NodeAggregateId::fromString($nodeAggregateId));
if ($this->fusionContext['node'] === null) {
throw new InvalidArgumentException(sprintf('Node with aggregate id "%s" could not be found in the current subgraph', $nodeAggregateId), 1696700222);
}
$this->fusionContext['documentNode'] = $subgraph->findClosestNode(NodeAggregateId::fromString($nodeAggregateId), FindClosestNodeFilter::create('Neos.Neos:Document'));
if ($this->fusionContext['documentNode'] === null) {
throw new \RuntimeException(sprintf('Failed to find closest document node for node with aggregate id "%s"', $nodeAggregateId), 1697790940);
}
$this->fusionContext['site'] = $subgraph->findClosestNode($this->fusionContext['documentNode']->nodeAggregateId, FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_SITE));
if ($this->fusionContext['site'] === null) {
throw new \RuntimeException(sprintf('Failed to resolve site node for node with aggregate id "%s"', $nodeAggregateId), 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->fusionGlobalContext['request'] = ActionRequest::fromHttpRequest($httpRequest);
}

/**
* @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 (isset($this->fusionGlobalContext['request'])) {
$requestHandler = new FunctionalTestRequestHandler(self::$bootstrap);
$requestHandler->setHttpRequest($this->fusionGlobalContext['request']->getHttpRequest());
}
$this->throwExceptionIfLastRenderingLedToAnError();
$this->renderingResult = null;
$fusionAst = (new Parser())->parseFromSource(FusionSourceCodeCollection::fromString($this->fusionCode . chr(10) . $fusionCode->getRaw()));

$fusionGlobals = FusionGlobals::fromArray($this->fusionGlobalContext);

$fusionRuntime = (new RuntimeFactory())->createFromConfiguration($fusionAst, $fusionGlobals);
$fusionRuntime->overrideExceptionHandler($this->getObjectManager()->get(ThrowingHandler::class));
$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);
}
}

}
96 changes: 96 additions & 0 deletions Neos.Neos/Tests/Behavior/Features/Fusion/ContentCase.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
@fixtures
Feature: Tests for the "Neos.Neos:ContentCase" Fusion prototype

Background:
Given using no content dimensions
And using the following node types:
"""yaml
'Neos.ContentRepository:Root': {}
'Neos.Neos:Sites':
superTypes:
'Neos.ContentRepository:Root': true
'Neos.Neos:Document':
properties:
title:
type: string
uriPathSegment:
type: string
'Neos.Neos:Site':
superTypes:
'Neos.Neos:Document': true
'Neos.Neos:Test.DocumentType1':
superTypes:
'Neos.Neos:Document': true
'Neos.Neos:Test.DocumentType2':
superTypes:
'Neos.Neos:Document': true
"""
And using identifier "default", I define a content repository
And I am in content repository "default"
And I am user identified by "initiating-user-identifier"

When the command CreateRootWorkspace is executed with payload:
| Key | Value |
| workspaceName | "live" |
| newContentStreamId | "cs-identifier" |
And the command CreateRootNodeAggregateWithNode is executed with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "root" |
| nodeTypeName | "Neos.Neos:Sites" |
And the graph projection is fully up to date
And I am in content stream "cs-identifier" and dimension space point {}
And the following CreateNodeAggregateWithNode commands are executed:
| nodeAggregateId | parentNodeAggregateId | nodeTypeName |
| a | root | Neos.Neos:Site |
| a1 | a | Neos.Neos:Test.DocumentType2 |
And A site exists for node name "a" and domain "http://localhost"
And the sites configuration is:
"""yaml
Neos:
Neos:
sites:
'*':
contentRepository: default
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
"""
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
"""

0 comments on commit 4f9efcf

Please sign in to comment.