Skip to content

Commit f1a7aa9

Browse files
authored
[TASK] add more test for TemplateParser (#1367)
1 parent 96c6569 commit f1a7aa9

File tree

9 files changed

+314
-2
lines changed

9 files changed

+314
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ vendor
66
examples/cache/
77
/Documentation-GENERATED-temp/
88
infection.html
9+
infection.log

infection.json5

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"threads": "max",
1616
"initialTestsPhpOptions": "-d xdebug.mode=coverage",
1717
"logs": {
18-
"html": "infection.html"
18+
"html": "infection.html",
19+
"text": "infection.log"
1920
}
2021
}

tests/Functional/Core/Component/AbstractComponentCollectionTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ public static function getComponentDefinitionDataProvider(): iterable
127127
['test1', 'test2', 'default'],
128128
),
129129
],
130+
[
131+
'mixedSlots',
132+
new ComponentDefinition(
133+
'mixedSlots',
134+
[],
135+
false,
136+
['outer', 'inner'],
137+
),
138+
],
130139
];
131140
}
132141

@@ -241,6 +250,7 @@ public static function getAvailableComponentsDataProvider(): array
241250
'enumTypeArgumentWithDefault',
242251
'globalNamespaceUsage',
243252
'localNamespaceImport',
253+
'mixedSlots',
244254
'namedSlots',
245255
'namespace.test',
246256
'nested.subComponent',
@@ -267,6 +277,7 @@ public static function getAvailableComponentsDataProvider(): array
267277
'enumTypeArgumentWithDefault.enumTypeArgumentWithDefault',
268278
'globalNamespaceUsage.globalNamespaceUsage',
269279
'localNamespaceImport.localNamespaceImport',
280+
'mixedSlots.mixedSlots',
270281
'namedSlots.namedSlots',
271282
'namespace.test.test',
272283
'nested.subComponent.subComponent',

tests/Functional/Core/Parser/TemplateParserTest.php

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use PHPUnit\Framework\Attributes\Test;
1414
use TYPO3Fluid\Fluid\Core\Compiler\StopCompilingException;
1515
use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
16+
use TYPO3Fluid\Fluid\Core\ErrorHandler\ErrorHandlerInterface;
17+
use TYPO3Fluid\Fluid\Core\ErrorHandler\TolerantErrorHandler;
1618
use TYPO3Fluid\Fluid\Core\Parser\Exception as ParserException;
1719
use TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface;
1820
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
@@ -27,6 +29,7 @@
2729
use TYPO3Fluid\Fluid\Core\Parser\TemplateProcessorInterface;
2830
use TYPO3Fluid\Fluid\Core\Parser\UnknownNamespaceException;
2931
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext;
32+
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception as ViewHelperException;
3033
use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase;
3134
use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\ParserConfigurationAccessRenderingContext;
3235

@@ -101,13 +104,92 @@ public function invalidViewHelperCallThrowsException(string $source, int $except
101104
$subject->parse($source);
102105
}
103106

107+
#[Test]
108+
public function parseExceptionUsesOriginalTemplatePathForContext(): void
109+
{
110+
$renderingContext = new RenderingContext();
111+
$subject = $renderingContext->getTemplateParser();
112+
113+
try {
114+
$subject->parse('<f:render>', 'identifier-name', '/path/to/original/template.html');
115+
self::fail('Expected parse() to throw for invalid template source.');
116+
} catch (ParserException $exception) {
117+
self::assertStringContainsString(
118+
'Fluid parse error in template /path/to/original/template.html',
119+
$exception->getMessage(),
120+
);
121+
self::assertStringNotContainsString(
122+
'Fluid parse error in template identifier-name',
123+
$exception->getMessage(),
124+
);
125+
self::assertSame('/path/to/original/template.html', $exception->getTemplateLocation()->identifierOrPath);
126+
}
127+
}
128+
129+
#[Test]
130+
public function parseExceptionStartsAtFirstLineAndCharacterAfterReset(): void
131+
{
132+
$renderingContext = new RenderingContext();
133+
$subject = $renderingContext->getTemplateParser();
134+
135+
try {
136+
$subject->parse('<f:render>');
137+
self::fail('Expected parse() to throw for invalid template source.');
138+
} catch (ParserException $exception) {
139+
self::assertStringContainsString('line 1 at character 1', $exception->getMessage());
140+
self::assertSame(1, $exception->getTemplateLocation()->line);
141+
self::assertSame(1, $exception->getTemplateLocation()->character);
142+
}
143+
}
144+
145+
#[Test]
146+
public function parseExceptionInViewHelperArgumentUsesOuterTemplateChunk(): void
147+
{
148+
$renderingContext = new RenderingContext();
149+
$renderingContext->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers');
150+
$subject = $renderingContext->getTemplateParser();
151+
152+
try {
153+
$subject->parse('<test:requiredArgument required="{invalid:foo()}" />');
154+
self::fail('Expected parse() to throw for invalid template source.');
155+
} catch (ParserException $exception) {
156+
self::assertStringContainsString(
157+
'Template source chunk: <test:requiredArgument required="{invalid:foo()}" />',
158+
$exception->getMessage(),
159+
);
160+
self::assertStringNotContainsString(
161+
'Template source chunk: {invalid:foo()}',
162+
$exception->getMessage(),
163+
);
164+
}
165+
}
166+
167+
#[Test]
168+
public function parseExceptionAfterMultilinePreviousBlockUsesLastLineCharacterOffset(): void
169+
{
170+
$renderingContext = new RenderingContext();
171+
$subject = $renderingContext->getTemplateParser();
172+
173+
try {
174+
$subject->parse("long-prefix\nx\n<f:render>");
175+
self::fail('Expected parse() to throw for invalid template source.');
176+
} catch (ParserException $exception) {
177+
self::assertStringContainsString('line 3 at character 2', $exception->getMessage());
178+
self::assertStringNotContainsString('line 3 at character 14', $exception->getMessage());
179+
self::assertSame(3, $exception->getTemplateLocation()->line);
180+
self::assertSame(2, $exception->getTemplateLocation()->character);
181+
}
182+
}
183+
104184
#[Test]
105185
public function providedRequiredViewHelperArgumentThrowsNoException(): void
106186
{
107187
$renderingContext = new RenderingContext();
108188
$renderingContext->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers');
109189
$subject = $renderingContext->getTemplateParser();
110-
self::assertInstanceOf(ParsingState::class, $subject->parse('<test:requiredArgument required="test" />'));
190+
$parsedTemplate = $subject->parse('<test:requiredArgument required="test" />', 'identifier-name');
191+
self::assertInstanceOf(ParsingState::class, $parsedTemplate);
192+
self::assertSame('identifier-name', $parsedTemplate->getIdentifier());
111193
}
112194

113195
public static function validateAdditionalArgumentsGetsCalledWithUndefinedArgumentsDataProvider(): array
@@ -139,6 +221,62 @@ public function validateAdditionalArgumentsGetsCalledWithUndefinedArguments(stri
139221
$subject->parse($source);
140222
}
141223

224+
#[Test]
225+
public function parserErrorsThrownAfterInitializationInOpeningTagsAreHandledAsTextByTolerantErrorHandler(): void
226+
{
227+
$mockInterceptor = self::createMock(InterceptorInterface::class);
228+
$mockInterceptor->expects(self::once())
229+
->method('process')
230+
->willThrowException(new ParserException('interceptor failure'));
231+
$mockInterceptor->expects(self::once())
232+
->method('getInterceptionPoints')
233+
->willReturn([InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER]);
234+
235+
$renderingContext = new ParserConfigurationAccessRenderingContext();
236+
$renderingContext->setErrorHandler(new TolerantErrorHandler());
237+
$renderingContext->parserConfiguration->addInterceptor($mockInterceptor);
238+
$renderingContext->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers');
239+
$subject = $renderingContext->getTemplateParser();
240+
241+
$parsedTemplate = $subject->parse('<test:requiredArgument required="test" />');
242+
243+
/** @var TextNode */
244+
$textNode = $parsedTemplate->getRootNode()->getChildNodes()[0];
245+
self::assertInstanceOf(TextNode::class, $textNode);
246+
self::assertStringContainsString(
247+
'Parser error: interceptor failure Offending code: ',
248+
$textNode->getText(),
249+
);
250+
}
251+
252+
#[Test]
253+
public function viewHelperErrorsThrownAfterInitializationInOpeningTagsAreHandledAsTextByTolerantErrorHandler(): void
254+
{
255+
$mockInterceptor = self::createMock(InterceptorInterface::class);
256+
$mockInterceptor->expects(self::once())
257+
->method('process')
258+
->willThrowException(new ViewHelperException('interceptor failure'));
259+
$mockInterceptor->expects(self::once())
260+
->method('getInterceptionPoints')
261+
->willReturn([InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER]);
262+
263+
$renderingContext = new ParserConfigurationAccessRenderingContext();
264+
$renderingContext->setErrorHandler(new TolerantErrorHandler());
265+
$renderingContext->parserConfiguration->addInterceptor($mockInterceptor);
266+
$renderingContext->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers');
267+
$subject = $renderingContext->getTemplateParser();
268+
269+
$parsedTemplate = $subject->parse('<test:requiredArgument required="test" />');
270+
271+
/** @var TextNode */
272+
$textNode = $parsedTemplate->getRootNode()->getChildNodes()[0];
273+
self::assertInstanceOf(TextNode::class, $textNode);
274+
self::assertStringContainsString(
275+
'ViewHelper error: interceptor failure - Offending code: ',
276+
$textNode->getText(),
277+
);
278+
}
279+
142280
public static function viewHelperArgumentsGetParsedCorrectlyDataProvider(): iterable
143281
{
144282
yield ['<test:arbitraryArguments />', []];
@@ -357,6 +495,21 @@ public function objectAccessorNodesAreNotEscapedIfEscapingIsDisabled(): void
357495
self::assertSame('foo.bar', $objectAccessorNode->getObjectPath());
358496
}
359497

498+
#[Test]
499+
public function parseResetsEscapingStateBetweenCalls(): void
500+
{
501+
$renderingContext = new RenderingContext();
502+
$subject = $renderingContext->getTemplateParser();
503+
504+
$subject->parse('{escaping=false}{foo.bar}');
505+
$parsedTemplate = $subject->parse('{foo.bar}');
506+
507+
/** @var EscapingNode */
508+
$escapingNode = $parsedTemplate->getRootNode()->getChildNodes()[0];
509+
self::assertInstanceOf(EscapingNode::class, $escapingNode);
510+
self::assertInstanceOf(ObjectAccessorNode::class, $escapingNode->getNode());
511+
}
512+
360513
#[Test]
361514
public function objectAccessorNodesAreRunThroughInterceptors(): void
362515
{
@@ -435,8 +588,14 @@ public function getOrParseAndStoreTemplateSetsAndStoresUncompilableStateInCache(
435588
// Second try with uncompilable flag
436589
'',
437590
);
591+
$mockErrorHandler = $this->createMock(ErrorHandlerInterface::class);
592+
$mockErrorHandler->expects(self::once())
593+
->method('handleCompilerError')
594+
->with(self::isInstanceOf(StopCompilingException::class))
595+
->willReturn('');
438596

439597
$renderingContext = new RenderingContext();
598+
$renderingContext->setErrorHandler($mockErrorHandler);
440599
$renderingContext->setTemplateCompiler($mockCompiler);
441600
$subject = $renderingContext->getTemplateParser();
442601
$parsedTemplate = $subject->getOrParseAndStoreTemplate(

tests/Functional/Core/Parser/ViewHelperNamespacesTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use PHPUnit\Framework\Attributes\DataProvider;
1313
use PHPUnit\Framework\Attributes\Test;
14+
use TYPO3Fluid\Fluid\Core\Parser\UnknownNamespaceException;
1415
use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase;
1516
use TYPO3Fluid\Fluid\View\TemplateView;
1617

@@ -154,4 +155,25 @@ public function inlineViewHelperSyntaxIgnoredNamespace(string $source, string $e
154155
$output = $view->render();
155156
self::assertSame($cacheBustingPrefix . $source, $output);
156157
}
158+
159+
#[Test]
160+
public function inlineViewHelperSyntaxIgnoredNamespaceDoesNotHideInvalidNamespaceInChain(): void
161+
{
162+
$source = '{value -> ignored:foo() -> invalid:bar()}';
163+
164+
foreach ([1, 2] as $_) {
165+
$view = new TemplateView();
166+
$view->assign('value', 'value');
167+
$view->getRenderingContext()->setCache(self::$cache);
168+
$view->getRenderingContext()->getViewHelperResolver()->addNamespace('ignored', null);
169+
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($source);
170+
171+
try {
172+
$view->render();
173+
self::fail('Expected render() to throw for invalid namespace in mixed shorthand chain.');
174+
} catch (UnknownNamespaceException $exception) {
175+
self::assertStringContainsString('Unknown Namespace: invalid', $exception->getMessage());
176+
}
177+
}
178+
}
157179
}

tests/Functional/Core/ViewHelper/ViewHelperArgumentTypesTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,29 @@ public function scalarArguments(string $argumentName, mixed $argumentValue, mixe
162162
self::assertSame($expectedValue, $result[$argumentName], 'cached');
163163
}
164164

165+
#[Test]
166+
public function escapedBooleanArgumentStaysFalseForQuotedEmptyStringVariable(): void
167+
{
168+
$variables = ['argumentValue' => '""'];
169+
$source = '<test:escapedBooleanArgument flag="{argumentValue}" />';
170+
171+
$view = new TemplateView();
172+
$view->getRenderingContext()->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\\Fluid\\Tests\\Functional\\Fixtures\\ViewHelpers');
173+
$view->assignMultiple($variables);
174+
$view->getRenderingContext()->setCache(self::$cache);
175+
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($source);
176+
$result = unserialize($view->render());
177+
self::assertFalse($result['flag'], 'uncached');
178+
179+
$view = new TemplateView();
180+
$view->getRenderingContext()->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\\Fluid\\Tests\\Functional\\Fixtures\\ViewHelpers');
181+
$view->assignMultiple($variables);
182+
$view->getRenderingContext()->setCache(self::$cache);
183+
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($source);
184+
$result = unserialize($view->render());
185+
self::assertFalse($result['flag'], 'cached');
186+
}
187+
165188
public static function unionTypesDataProvider(): array
166189
{
167190
return [
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<f:slot name="outer" />
2+
{f:if(condition: '{f:slot(name: \'inner\')} === {null}', then: 'inner slot missing', else: '{f:slot(name: \'inner\')}')}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file belongs to the package "TYPO3 Fluid".
7+
* See LICENSE.txt that was shipped with this package.
8+
*/
9+
10+
namespace TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers;
11+
12+
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
13+
14+
final class EscapedBooleanArgumentViewHelper extends AbstractViewHelper
15+
{
16+
protected $escapeOutput = false;
17+
18+
public function initializeArguments(): void
19+
{
20+
$this->registerArgument('flag', 'bool', '', false, null, true);
21+
}
22+
23+
public function render(): string
24+
{
25+
return serialize($this->arguments);
26+
}
27+
}

0 commit comments

Comments
 (0)