diff --git a/Classes/Controller/DateController.php b/Classes/Controller/DateController.php index 4cfd0fb..338d6f7 100644 --- a/Classes/Controller/DateController.php +++ b/Classes/Controller/DateController.php @@ -7,6 +7,7 @@ namespace WerkraumMedia\Events\Controller; use Exception; use Psr\Http\Message\ResponseInterface; use Throwable; +use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Extbase\Annotation as Extbase; use TYPO3\CMS\Extbase\Service\ExtensionService; use WerkraumMedia\Events\Domain\Model\Date; @@ -38,13 +39,13 @@ final class DateController extends AbstractController { parent::initializeAction(); + $this->handlePostRequests(); + $contentObject = $this->request->getAttribute('currentContentObject'); if ($contentObject !== null) { $this->demandFactory->setContentObjectRenderer($contentObject); } $this->dataProcessing->setConfigurationManager($this->configurationManager); - - $this->handlePostRequest(); } public function listAction( @@ -127,22 +128,34 @@ final class DateController extends AbstractController * * @see: https://en.wikipedia.org/wiki/Post/Redirect/Get */ - private function handlePostRequest(): void + private function handlePostRequests(): void { - if ( - $this->request->getMethod() === 'POST' - && $this->request->hasArgument('search') - && is_array($this->request->getArgument('search')) - ) { - $namespace = $this->extensionService->getPluginNamespace(null, null); + if ($this->request->getMethod() !== 'POST') { + return; + } + + $searchArguments = []; + if ($this->request->hasArgument('search')) { + $searchArguments = $this->request->getArgument('search'); + } + if (is_array($searchArguments) === false) { + $searchArguments = []; + } + $searchArguments = array_filter($searchArguments); + + $parameter = []; + if ($searchArguments !== []) { + $parameter['search'] = $searchArguments; + } + + $namespace = $this->extensionService->getPluginNamespace(null, null); + + throw new PropagateResponseException( $this->redirectToUri($this->request->getAttribute('currentContentObject')->typoLink_URL([ 'parameter' => 't3://page?uid=current', - 'additionalParams' => '&' . http_build_query([ - $namespace => [ - 'search' => array_filter($this->request->getArgument('search')), - ], - ]), - ])); - } + 'additionalParams' => '&' . http_build_query([$namespace => $parameter]), + ])), + 303 + ); } } diff --git a/Classes/Middleware/AddSearchArgumentsToRouteArgumentsMiddleware.php b/Classes/Middleware/AddSearchArgumentsToRouteArgumentsMiddleware.php new file mode 100644 index 0000000..fe00a53 --- /dev/null +++ b/Classes/Middleware/AddSearchArgumentsToRouteArgumentsMiddleware.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +/* + * Copyright (C) 2025 Daniel Siepmann <daniel.siepmann@codappix.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace WerkraumMedia\Events\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Core\Routing\PageArguments; +use TYPO3\CMS\Core\Utility\ArrayUtility; + +/** + * Right now I couldn't find any way to add the search parameter to the pagination via addQueryString. + * The issue is that these are not part of TYPO3 routing. Nested lists like categories also seem impossible to configure for routing. + * + * See corresponding core issue: https://forge.typo3.org/issues/105941 + */ +final class AddSearchArgumentsToRouteArgumentsMiddleware implements MiddlewareInterface +{ + public function __construct( + private readonly string $routingPath = 'events/search', + ) { + } + + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $previousResult = $request->getAttribute('routing', null); + if ($previousResult instanceof PageArguments) { + $request = $request->withAttribute('routing', $this->extendPageArguments($previousResult)); + } + + return $handler->handle($request); + } + + private function extendPageArguments(PageArguments $pageArguments): PageArguments + { + $dynamicArguments = $pageArguments->getDynamicArguments(); + + if (ArrayUtility::isValidPath($dynamicArguments, $this->routingPath) === false) { + return $pageArguments; + } + + $routeArguments = ArrayUtility::setValueByPath( + $pageArguments->getRouteArguments(), + $this->routingPath, + ArrayUtility::getValueByPath($dynamicArguments, $this->routingPath) + ); + $remainingArguments = ArrayUtility::removeByPath($dynamicArguments, $this->routingPath); + + return new PageArguments( + $pageArguments->getPageId(), + $pageArguments->getPageType(), + $routeArguments, + $pageArguments->getStaticArguments(), + $remainingArguments, + ); + } +} diff --git a/Configuration/CeRouting.yaml b/Configuration/CeRouting.yaml index 3137846..97e80f3 100644 --- a/Configuration/CeRouting.yaml +++ b/Configuration/CeRouting.yaml @@ -17,7 +17,9 @@ routeEnhancers: EventsPagination: type: Plugin namespace: events - routePath: '/{localizedPage}-{currentPage}' + routePath: '/{localizedPage}-{currentPage}/{controller}' + defaults: + controller: 'Date' aspects: localizedPage: type: LocaleModifier diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php new file mode 100644 index 0000000..453090b --- /dev/null +++ b/Configuration/RequestMiddlewares.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +use WerkraumMedia\Events\Middleware\AddSearchArgumentsToRouteArgumentsMiddleware; + +return [ + 'frontend' => [ + 'werkraummedia/events/add-search-arguments-to-route-arguments' => [ + 'target' => AddSearchArgumentsToRouteArgumentsMiddleware::class, + 'after' => [ + 'typo3/cms-frontend/page-resolver', + ], + 'before' => [ + 'typo3/cms-frontend/page-argument-validator', + ], + ], + ], +]; diff --git a/Documentation/Changelog/5.0.0.rst b/Documentation/Changelog/5.0.0.rst index 4fe4e45..b03307b 100644 --- a/Documentation/Changelog/5.0.0.rst +++ b/Documentation/Changelog/5.0.0.rst @@ -44,6 +44,9 @@ Fixes * Ensure pagination settings are provided in expected type (int). TypoScript settings will always be strings, so we fix this with a proper type cast. +* Fix broken pagination routing in combination with active search for TYPO3 12.4 and onwards. + See: :ref:`searchPagination`. + Tasks ----- diff --git a/Documentation/Features/SearchPagination.rst b/Documentation/Features/SearchPagination.rst new file mode 100644 index 0000000..b344324 --- /dev/null +++ b/Documentation/Features/SearchPagination.rst @@ -0,0 +1,25 @@ +.. index:: single: Search +.. _searchPagination: + +Search Pagination +================= + +The extension supports pagination of search results. +Please properly configure the system. Add the following `TYPO3_CONF_VARS` configuration: + +.. code-block:: php + + 'FE' => [ + 'cacheHash' => [ + 'excludedParameters' => [ + '^events[search]', + ], + ], + ], + +Adopt the configuration to your own setup, e.g. change the `pluginNamespace` from `events` to whatever you've configured. +And ensure the involved plugins are excluded from caching (`USER_INT`). + +The extension will assume `events[search]` as default namespace for search arguments. +Please make use of Services files and Dependency Injection to configure the custom +`AddSearchArgumentsToRouteArgumentsMiddleware` middleware with your own namespace. diff --git a/Tests/Functional/Frontend/Fixtures/Database/SearchSetup.php b/Tests/Functional/Frontend/Fixtures/Database/SearchSetup.php new file mode 100644 index 0000000..42b4d80 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Database/SearchSetup.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +return [ + 'tt_content' => [ + [ + 'pid' => '1', + 'uid' => '1', + 'CType' => 'cefilter_filter', + 'header' => 'Search Form', + ], + [ + 'pid' => '1', + 'uid' => '2', + 'CType' => 'celist_list', + 'header' => 'Search Results', + ], + ], + 'tx_events_domain_model_event' => [ + [ + 'uid' => '1', + 'pid' => '2', + 'title' => 'Event one', + 'teaser' => 'Some Teaser', + ], + [ + 'uid' => '2', + 'pid' => '2', + 'title' => 'Event two', + 'teaser' => 'Another teaser', + ], + ], + 'tx_events_domain_model_date' => [ + [ + 'uid' => '1', + 'pid' => '2', + 'event' => '1', + 'start' => '1661626800', + 'end' => '1661632200', + ], + [ + 'uid' => '2', + 'pid' => '2', + 'event' => '1', + 'start' => '1660158000', + 'end' => '1660163400', + ], + [ + 'uid' => '3', + 'pid' => '2', + 'event' => '2', + 'start' => '1661194800', + 'end' => '1661200200', + ], + ], +]; diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/Configuration/TypoScript/Setup.typoscript b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/Configuration/TypoScript/Setup.typoscript new file mode 100644 index 0000000..47f670c --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/Configuration/TypoScript/Setup.typoscript @@ -0,0 +1,9 @@ +tt_content.cefilter_filter.20 { + view { + pluginNamespace = events + } + + persistence { + storagePid = 2 + } +} diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/Resources/Private/Templates/Date/Search.html b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/Resources/Private/Templates/Date/Search.html new file mode 100644 index 0000000..94090ae --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/Resources/Private/Templates/Date/Search.html @@ -0,0 +1,53 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> + + <div class="row"> + <div class="col-12 col-md-8 offset-md-4"> + <div class="panel panel-default"> + <div class="panel-body"> + <f:form method="post" name="search" object="{demand}"> + <div class="row mb-3"> + <div class="col-md-12"> + <div class="form-group"> + <label for="searchword">Searchword</label> + <f:form.textfield type="text" class="form-control" id="searchword" property="searchword" value="{searchword}" /> + </div> + </div> + </div> + <div class="row mb-3"> + <div class="col col-md-6"> + <div class="form-group"> + <label for="start">From:</label> + <f:form.textfield type="date" class="form-control" id="start" property="start" /> + </div> + </div> + <div class="col col-md-6"> + <div class="form-group"> + <label for="end">To:</label> + <f:form.textfield type="date" class="form-control" id="end" property="end" /> + </div> + </div> + </div> + + <f:if condition="{categories}"> + <div class="row mb-3"> + <f:for each="{categories}" as="category"> + <div class="col-md-4 d-none d-lg-block"> + <div class="form-check"> + <f:form.checkbox class="form-check-input" property="userCategories" value="{category.uid}" id="check_{category.uid}"/> + <label class="form-check-label" for="check_{category.uid}">{category.title} {category.amountOfEvents}</label> + </div> + </div> + </f:for> + </div> + </f:if> + + <div class="form-group mb-3"> + <f:form.submit value="Search" class="btn btn-primary" /> + </div> + </f:form> + </div> + </div> + </div> + </div> + +</html> diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/composer.json b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/composer.json new file mode 100644 index 0000000..ecd35d6 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/composer.json @@ -0,0 +1,16 @@ +{ + "name": "werkraummedia/ce-events-filter", + "type": "typo3-cms-extension", + "description": "Content element: EXT:events filter", + "license": "GPL-2.0-or-later", + "version": "v1.0.0", + "require": { + "typo3/cms-core": "*", + "typo3/cms-extbase": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "ce_filter" + } + } +} diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/ext_emconf.php b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/ext_emconf.php new file mode 100644 index 0000000..2e39e72 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/ext_emconf.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +$EM_CONF['ce_filter'] = [ + 'title' => 'Events filter', + 'description' => 'Content Element for \'Events\'', + 'category' => 'plugin', + 'version' => '1.0.0', + 'constraints' => [ + 'depends' => [ + 'typo3' => '*', + 'events' => '*', + ], + 'conflicts' => [ + ], + 'suggests' => [ + ], + ], +]; diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/ext_localconf.php b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/ext_localconf.php new file mode 100644 index 0000000..f3a0251 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/ext_localconf.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup( + '@import "EXT:ce_filter/Configuration/TypoScript/Setup.typoscript"' +); + +\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( + 'CeFilter', + 'filter', + [ + \WerkraumMedia\Events\Controller\DateController::class => 'search', + ], + [ + \WerkraumMedia\Events\Controller\DateController::class => 'search', + ], + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT +); diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Configuration/TypoScript/Setup.typoscript b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Configuration/TypoScript/Setup.typoscript new file mode 100644 index 0000000..fab3308 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Configuration/TypoScript/Setup.typoscript @@ -0,0 +1,15 @@ +tt_content.celist_list.20 { + view { + pluginNamespace = events + } + + persistence { + storagePid = 2 + } + + settings { + sortByDate = start + sortOrder = ASC + itemsPerPage = 1 + } +} diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Resources/Private/Partials/Pagination.html b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Resources/Private/Partials/Pagination.html new file mode 100644 index 0000000..48f68e2 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Resources/Private/Partials/Pagination.html @@ -0,0 +1,94 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" + data-namespace-typo3-fluid="true"> + + <nav role="navigation" aria-label="Pagination Navigation"> + <ul class="pagination"> + <f:if condition="{pagination.previousPageNumber}"> + <li class="page-item"> + <f:if condition="{pagination.previousPageNumber} > 1"> + <f:then> + <a class="page-link" + href="{f:uri.action(addQueryString: 1, arguments: {currentPage: pagination.previousPageNumber})}" + > + <span aria-hidden="true">«</span> + </a> + </f:then> + <f:else> + <a class="page-link" href="{f:uri.action(addQueryString: 1)}"> + <span aria-hidden="true">«</span> + </a> + </f:else> + </f:if> + </li> + </f:if> + + <f:if condition="{pagination.displayRangeStart} > 1"> + <li class="page-item"> + <a class="page-link" + href="{f:uri.action(addQueryString: 1)}" + aria-label="Goto Page 1" + > + 1 + </a> + </li> + </f:if> + + <f:if condition="{pagination.hasLessPages}"> + <li class="page-item"> + <span class="page-link">…</span> + </li> + </f:if> + + <f:for each="{pagination.allPageNumbers}" as="page"> + <f:if condition="{page} == {pagination.paginator.currentPageNumber}"> + <f:then> + <li class="page-item active"> + <span class="page-link" + aria-label="Current Page {page}" + aria-current="true" + >{page}</span> + </li> + </f:then> + <f:else> + <li class="page-item"> + <a class="page-link" + href="{f:uri.action(addQueryString: 1, arguments: {currentPage: page})}" + aria-label="Goto Page {page}" + > + {page} + </a> + </li> + </f:else> + </f:if> + </f:for> + + <f:if condition="{pagination.hasMorePages}"> + <li class="page-item"> + <span class="page-link">…</span> + </li> + </f:if> + + <f:if condition="{pagination.displayRangeEnd} < {pagination.lastPageNumber}"> + <li class="page-item"> + <a class="page-link" + href="{f:uri.action(addQueryString: 1, arguments: {currentPage: pagination.lastPageNumber})}" + aria-label="Goto Page {pagination.lastPageNumber}" + > + {pagination.lastPageNumber} + </a> + </li> + </f:if> + + <f:if condition="{pagination.nextPageNumber}"> + <li class="page-item"> + <a class="page-link" + href="{f:uri.action(addQueryString: 1, arguments: {currentPage: pagination.nextPageNumber})}" + > + <span aria-hidden="true">»</span> + </a> + </li> + </f:if> + </ul> + </nav> + +</html> diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Resources/Private/Templates/Date/List.html b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Resources/Private/Templates/Date/List.html new file mode 100644 index 0000000..3bfb675 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/Resources/Private/Templates/Date/List.html @@ -0,0 +1,29 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> + +<f:if condition="{pagination.paginator.paginatedItems}"> + <section class="events-list" data-uniform="height"> + + <f:for each="{pagination.paginator.paginatedItems}" as="date" iteration="index"> + <div class="row mb-5"> + <h3> + {date.event.title} + </h3> + + <p> + <f:format.crop maxCharacters="160" respectHtml="true" + respectWordBoundaries="true"> + {date.event.teaser}</f:format.crop> + </p> + </div> + </f:for> + + </section> + + <section class="events-list-pagination"> + {f:render(partial: 'Pagination', arguments: { + pagination: pagination + })} + </section> +</f:if> + +</html> diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/composer.json b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/composer.json new file mode 100644 index 0000000..d3ab706 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/composer.json @@ -0,0 +1,16 @@ +{ + "name": "werkraummedia/ce-list", + "type": "typo3-cms-extension", + "description": "Content element: EXT:events list", + "license": "GPL-2.0-or-later", + "version": "v1.0.0", + "require": { + "typo3/cms-core": "*", + "typo3/cms-extbase": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "ce_list" + } + } +} diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/ext_emconf.php b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/ext_emconf.php new file mode 100644 index 0000000..82aa72d --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/ext_emconf.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +$EM_CONF['ce_list'] = [ + 'title' => 'Events list view', + 'description' => 'Content Elements for \'Events\'', + 'category' => 'plugin', + 'version' => '1.0.0', + 'constraints' => [ + 'depends' => [ + 'typo3' => '*', + 'events' => '*', + ], + 'conflicts' => [ + ], + 'suggests' => [ + ], + ], +]; diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/ext_localconf.php b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/ext_localconf.php new file mode 100644 index 0000000..8282237 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/ext_localconf.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup( + '@import "EXT:ce_list/Configuration/TypoScript/Setup.typoscript"' +); + +\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( + 'CeList', + 'list', + [ + \WerkraumMedia\Events\Controller\DateController::class => 'list', + ], + [ + \WerkraumMedia\Events\Controller\DateController::class => 'list', + ], + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT +); diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/example/Classes/TestingDateTimeAspectMiddleware.php b/Tests/Functional/Frontend/Fixtures/Extensions/example/Classes/TestingDateTimeAspectMiddleware.php new file mode 100644 index 0000000..64b4bac --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/example/Classes/TestingDateTimeAspectMiddleware.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +/* + * Copyright (C) 2025 Daniel Siepmann <daniel.siepmann@codappix.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace WerkraumMedia\EventsExample; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\DateTimeAspect; + +/** + * Allows us to set a specific DateTimeAspect for requests within tests. + */ +final class TestingDateTimeAspectMiddleware implements MiddlewareInterface +{ + public function __construct( + private readonly Context $context, + ) { + } + + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $testingDateAspect = $request->getAttribute('testingDateAspect'); + if ($testingDateAspect instanceof DateTimeAspect) { + $this->context->setAspect('date', $testingDateAspect); + } + + return $handler->handle($request); + } +} diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/example/Configuration/RequestMiddlewares.php b/Tests/Functional/Frontend/Fixtures/Extensions/example/Configuration/RequestMiddlewares.php new file mode 100644 index 0000000..53c4371 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/example/Configuration/RequestMiddlewares.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +use WerkraumMedia\EventsExample\TestingDateTimeAspectMiddleware; + +return [ + 'frontend' => [ + 'werkraummedia/events/testing-date-time-aspect' => [ + 'target' => TestingDateTimeAspectMiddleware::class, + 'before' => [ + 'typo3/cms-frontend/timetracker', + ], + ], + ], +]; diff --git a/Tests/Functional/Frontend/Fixtures/Extensions/example/Configuration/Services.php b/Tests/Functional/Frontend/Fixtures/Extensions/example/Configuration/Services.php new file mode 100644 index 0000000..f206382 --- /dev/null +++ b/Tests/Functional/Frontend/Fixtures/Extensions/example/Configuration/Services.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +return static function (ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services() + ->defaults() + ->autowire() + ->autoconfigure() + ; + + $services->load('WerkraumMedia\\EventsExample\\', '../Classes/*'); +}; diff --git a/Tests/Functional/Frontend/Fixtures/Sites/default/config.yaml b/Tests/Functional/Frontend/Fixtures/Sites/default/config.yaml index 5b610ef..50e1956 100644 --- a/Tests/Functional/Frontend/Fixtures/Sites/default/config.yaml +++ b/Tests/Functional/Frontend/Fixtures/Sites/default/config.yaml @@ -14,3 +14,6 @@ languages: flag: us websiteTitle: '' rootPageId: 1 +imports: + - + resource: 'EXT:events/Configuration/CeRouting.yaml' diff --git a/Tests/Functional/Frontend/SearchTest.php b/Tests/Functional/Frontend/SearchTest.php new file mode 100644 index 0000000..ea11956 --- /dev/null +++ b/Tests/Functional/Frontend/SearchTest.php @@ -0,0 +1,141 @@ +<?php + +declare(strict_types=1); + +namespace WerkraumMedia\Events\Tests\Functional\Frontend; + +use DateTimeImmutable; +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Core\Context\DateTimeAspect; +use TYPO3\CMS\Core\Http\StreamFactory; +use TYPO3\CMS\Core\Utility\ArrayUtility; +use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; + +final class SearchTest extends AbstractFrontendTestCase +{ + protected function setUp(): void + { + $this->testExtensionsToLoad = [ + ...$this->testExtensionsToLoad, + 'typo3conf/ext/events/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/', + 'typo3conf/ext/events/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/', + ]; + + ArrayUtility::mergeRecursiveWithOverrule($this->configurationToUseInTestInstance, [ + 'FE' => [ + 'cacheHash' => [ + 'excludedParameters' => [ + '^events[search]', + ], + ], + ], + ]); + + parent::setUp(); + + $this->importPHPDataSet(__DIR__ . '/Fixtures/Database/SearchSetup.php'); + } + + #[Test] + public function submittingPostWithoutSearchArgumentsRedirectsToGet(): void + { + $request = new InternalRequest('https://example.com/'); + $request = $request->withMethod('POST'); + $request = $request->withPageId(1); + + $response = $this->executeFrontendSubRequest($request); + + self::assertSame(303, $response->getStatusCode()); + self::assertSame('http://example.com/', $response->getHeaderLine('location')); + } + + #[Test] + public function submittingPostWithSearchArgumentsRedirectsToGet(): void + { + $request = new InternalRequest('https://example.com/'); + $request = $request->withMethod('POST'); + $request = $request->withPageId(1); + $request = $request->withBody((new StreamFactory())->createStream(http_build_query([ + 'events' => [ + 'search' => [ + 'searchword' => 'Event', + ], + ], + ]))); + + $response = $this->executeFrontendSubRequest($request); + + self::assertSame(303, $response->getStatusCode()); + self::assertSame('http://example.com/?events%5Bsearch%5D%5Bsearchword%5D=Event', $response->getHeaderLine('location')); + } + + #[Test] + public function submittedInputShownInForm(): void + { + $request = new InternalRequest('https://example.com/'); + $request = $request->withQueryParams([ + 'events' => [ + 'search' => [ + 'searchword' => 'Event', + ], + ], + ]); + $request = $request->withPageId(1); + $response = $this->executeFrontendSubRequest($request); + $html = $response->getBody()->__toString(); + + self::assertSame(200, $response->getStatusCode()); + self::assertStringContainsString('value="Event"', $html, 'Submitted value is not rendered within form'); + } + + #[Test] + public function submittedInputIsKeptWithinPagination(): void + { + $request = new InternalRequest('https://example.com/'); + $request = $request->withAttribute('testingDateAspect', new DateTimeAspect(new DateTimeImmutable('2022-08-10'))); + $request = $request->withQueryParams([ + 'events' => [ + 'search' => [ + 'searchword' => 'Event', + ], + ], + ]); + $request = $request->withPageId(1); + $response = $this->executeFrontendSubRequest($request); + $html = $response->getBody()->__toString(); + + self::assertSame(200, $response->getStatusCode()); + self::assertStringContainsString('Event one', $html); + self::assertStringContainsString('Current Page 1', $html); + self::assertStringContainsString('/page-2?events%5Bsearch%5D%5Bsearchword%5D=Event&cHash=41711281293c1c3a3aa161e96bbd4e98', $html); + self::assertStringNotContainsString('Event two', $html); + + self::assertStringContainsString('value="Event"', $html, 'Submitted value is not rendered within form'); + + // Ensure going to 2nd page works (make sure it is available after warming up cache for first page) + + $request = new InternalRequest('https://example.com/'); + $request = $request->withAttribute('testingDateAspect', new DateTimeAspect(new DateTimeImmutable('2022-08-10'))); + $request = $request->withQueryParams([ + 'events' => [ + 'search' => [ + 'searchword' => 'Event', + ], + 'controller' => 'Date', + 'currentPage' => '2', + ], + 'cHash' => '41711281293c1c3a3aa161e96bbd4e98', + ]); + $request = $request->withPageId(1); + $response = $this->executeFrontendSubRequest($request); + $html = $response->getBody()->__toString(); + + self::assertSame(200, $response->getStatusCode()); + self::assertStringContainsString('Event two', $html); + self::assertStringContainsString('Current Page 2', $html); + self::assertStringContainsString('/page-1?events%5Bsearch%5D%5Bsearchword%5D=Event&cHash=13c33adfef09ccb19da7d399ada25c4c', $html); + self::assertStringNotContainsString('Event one', $html); + + self::assertStringContainsString('value="Event"', $html, 'Submitted value is not rendered within form'); + } +} diff --git a/composer.json b/composer.json index 2bce866..9ec6243 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "autoload-dev": { "psr-4": { "WerkraumMedia\\Events\\Tests\\": "Tests", + "WerkraumMedia\\EventsExample\\": "Tests/Functional/Frontend/Fixtures/Extensions/example/Classes/", "WerkraumMedia\\CustomCategories\\": "Tests/Functional/Psr14Events/DestinationDataImport/Fixtures/Extensions/custom_categories/Classes/", "WerkraumMedia\\CustomEvent\\": "Tests/Functional/Psr14Events/DestinationDataImport/Fixtures/Extensions/custom_event/Classes/" }