diff --git a/.gitattributes b/.gitattributes index 3a01b37..c1108e7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ +/.github/ export-ignore +/Build/ export-ignore +/Tests/ export-ignore /.gitattributes export-ignore /.gitignore export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c0b0ea..a870292 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,13 +97,13 @@ jobs: -p ${{ matrix.packages.php }} \ -s composerValidate -# - name: Functional tests with sqlite -# run: | -# Build/Scripts/runTests.sh \ -# -p ${{ matrix.packages.php }} \ -# -d sqlite \ -# -s functional ${{ matrix.packages.testpath }} -# + - name: Functional tests with sqlite + run: | + Build/Scripts/runTests.sh \ + -p ${{ matrix.packages.php }} \ + -d sqlite \ + -s functional ${{ matrix.packages.testpath }} + # - name: Unit tests with sqlite # run: | # Build/Scripts/runTests.sh \ diff --git a/Build/Scripts/additionalTests.sh b/Build/Scripts/additionalTests.sh index 86b29a3..c809c7d 100755 --- a/Build/Scripts/additionalTests.sh +++ b/Build/Scripts/additionalTests.sh @@ -430,7 +430,6 @@ additionalCleanTestFiles() { rm -rf \ .cache \ bin/ \ - Build/phpunit \ Build/vendor/ \ Build/Web/ \ Documentation-GENERATED-temp/ \ diff --git a/Build/Scripts/test.sh b/Build/Scripts/test.sh index b6d332a..0fc66e7 100755 --- a/Build/Scripts/test.sh +++ b/Build/Scripts/test.sh @@ -102,11 +102,11 @@ runFunctionalTests () { -s composerValidate || exit 1 ; \ EXIT_CODE_VALIDATE=$? -# ./runTests.sh \ -# -p ${PHP_VERSION} \ -# -d sqlite \ -# -s functional ${TEST_PATH} || exit 1 ; \ -# EXIT_CODE_FUNCTIONAL=$? + ./runTests.sh \ + -p ${PHP_VERSION} \ + -d sqlite \ + -s functional ${TEST_PATH} || exit 1 ; \ + EXIT_CODE_FUNCTIONAL=$? echo "###########################################################################" >&2 echo " Finished unit and/or functional tests with" >&2 diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml new file mode 100644 index 0000000..0afe47f --- /dev/null +++ b/Build/phpunit/FunctionalTests.xml @@ -0,0 +1,46 @@ + + + + + + + ../../../../../../typo3/sysext/*/Tests/Functional/ + + + + + + + diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..9882f8f --- /dev/null +++ b/Build/phpunit/FunctionalTestsBootstrap.php @@ -0,0 +1,31 @@ +defineOriginalRootPath(); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); +})(); diff --git a/Classes/EventListener/BeforeContainerConfigurationIsAppliedListener.php b/Classes/EventListener/BeforeContainerConfigurationIsAppliedListener.php index dc67c61..5cc04e8 100644 --- a/Classes/EventListener/BeforeContainerConfigurationIsAppliedListener.php +++ b/Classes/EventListener/BeforeContainerConfigurationIsAppliedListener.php @@ -23,16 +23,8 @@ class BeforeContainerConfigurationIsAppliedListener #[AsEventListener('collapsible-container-beforecontainer', BeforeContainerConfigurationIsAppliedEvent::class)] public function __invoke(BeforeContainerConfigurationIsAppliedEvent $event): void { - $containerConfiguration = $event->getContainerConfiguration(); - - $containerConfiguration->setGridTemplate( - 'EXT:ew_collapsible_container/Resources/Private/Templates/Grid.html' + $event->getContainerConfiguration()->addGridPartialPath( + 'EXT:ew_collapsible_container/Resources/Private/Partials/' ); - - $containerConfiguration->setGridPartialPaths([ - 'EXT:backend/Resources/Private/Partials/', - 'EXT:container/Resources/Private/Partials/', - 'EXT:ew_collapsible_container/Resources/Private/Partials/', - ]); } } diff --git a/Classes/EventListener/BeforeContainerPreviewIsRenderedListener.php b/Classes/EventListener/BeforeContainerPreviewIsRenderedListener.php index a255886..dff3f5d 100644 --- a/Classes/EventListener/BeforeContainerPreviewIsRenderedListener.php +++ b/Classes/EventListener/BeforeContainerPreviewIsRenderedListener.php @@ -21,9 +21,15 @@ use Evoweb\EwCollapsibleContainer\Xclass\ContainerGridColumn; use TYPO3\CMS\Core\Attribute\AsEventListener; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction; +use TYPO3\CMS\Core\Page\PageRenderer; class BeforeContainerPreviewIsRenderedListener { + public function __construct(protected PageRenderer $pageRenderer) + { + } + #[AsEventListener('collapsible-container-beforepreview', BeforeContainerPreviewIsRenderedEvent::class)] public function __invoke(BeforeContainerPreviewIsRenderedEvent $event): void { @@ -38,6 +44,8 @@ public function __invoke(BeforeContainerPreviewIsRenderedEvent $event): void 'showMinItemsWarning' => $this->getShowMinItemsWarning($column, $countOfHiddenItems) ]); } + + $this->addFrontendResources(); } protected function getCountOfHiddenItems(ContainerGridColumn $columnObject): int @@ -71,6 +79,14 @@ protected function getShowMinItemsWarning(ContainerGridColumn $columnObject, int return $itemCount > 0 && ($itemCount - $hiddenItemCount) < $minItems; } + protected function addFrontendResources(): void + { + $this->pageRenderer->addCssFile('EXT:ew_collapsible_container/Resources/Public/Css/container.css'); + $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@evoweb/ew-collapsible-container/container.js') + ); + } + protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; diff --git a/Documentation/guides.xml b/Documentation/guides.xml index 8cc20c8..4268397 100644 --- a/Documentation/guides.xml +++ b/Documentation/guides.xml @@ -16,8 +16,8 @@ interlink-shortcode="evoweb/sf-books" /> diff --git a/Resources/Private/Partials/PageLayout/Grid/ColumnHeader.html b/Resources/Private/Partials/PageLayout/Grid/ColumnHeader.html index 4e167b1..f353971 100644 --- a/Resources/Private/Partials/PageLayout/Grid/ColumnHeader.html +++ b/Resources/Private/Partials/PageLayout/Grid/ColumnHeader.html @@ -39,22 +39,20 @@ - - - - - - - - + + + + + + @@ -73,14 +71,12 @@ - -
- - {f:translate(key: 'LLL:EXT:ew_collapsible_container/Resources/Private/Language/locallang.xlf:contentcollapsed')} -
-
+
+ + {f:translate(key: 'LLL:EXT:ew_collapsible_container/Resources/Private/Language/locallang.xlf:contentcollapsed')} +
diff --git a/Resources/Private/Templates/Grid.html b/Resources/Private/Templates/Grid.html deleted file mode 100644 index a6adcca..0000000 --- a/Resources/Private/Templates/Grid.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - diff --git a/Resources/Public/JavaScript/container.js b/Resources/Public/JavaScript/container.js index 6b625b6..cdf1a35 100644 --- a/Resources/Public/JavaScript/container.js +++ b/Resources/Public/JavaScript/container.js @@ -9,6 +9,7 @@ * LICENSE.txt file that was distributed with this source code. */ +import DocumentService from '@typo3/core/document-service.js'; import PersistentStorage from '@typo3/backend/storage/persistent.js'; class ContainerToggle { @@ -16,22 +17,21 @@ class ContainerToggle { columnExpand = '.t3js-expand-column'; + /** + * @type {PersistentStorage} + */ persistentStorage = null; storageKey = 'moduleData.list.containerExpanded'; constructor() { this.persistentStorage = PersistentStorage; - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - this.initialized(); - }); - } else { - this.initialized(); - } + DocumentService.ready().then(() => { + this.initialize(); + }); } - initialized() { + initialize() { this.initializeContainerToggle(); this.initializeExpandColumn(); } diff --git a/Tests/Fixtures/be_users.csv b/Tests/Fixtures/be_users.csv new file mode 100644 index 0000000..2603e55 --- /dev/null +++ b/Tests/Fixtures/be_users.csv @@ -0,0 +1,6 @@ +be_users,,,,,,,,,,,,,,,,, +,uid,pid,tstamp,username,password,admin,disable,starttime,endtime,options,crdate,workspace_perms,deleted,TSconfig,lastlogin,workspace_id,usergroup, +,1,0,1366642540,admin,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,1,0,0,0,0,1366642540,1,0,,1371033743,0,, +,2,0,1366642540,editor_no_group,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,,1371033743,0,, +,3,0,1366642540,editor_with_both_groups,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,,1371033743,0,"1,2", +,4,0,1366642540,editor_with_one_group,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,,1371033743,0,"2", diff --git a/Tests/Fixtures/tt_content.csv b/Tests/Fixtures/tt_content.csv new file mode 100644 index 0000000..587a7ab --- /dev/null +++ b/Tests/Fixtures/tt_content.csv @@ -0,0 +1,9 @@ +pages,,,, +,uid,pid,title,slug, +,1,0,Startpage,/startpage, +tt_content,,,,,, +,uid,pid,header,CType,colPos,tx_container_parent,hidden, +,1,1,Test Container,test-container,0,0,0, +,2,1,Test Child 1,test-child,200,1,0, +,3,1,Test Child 2,test-child,200,1,0, +,4,1,Test Child 3,test-child,200,1,1, diff --git a/Tests/Functional/EventListener/BeforeContainerConfigurationIsAppliedListenerTest.php b/Tests/Functional/EventListener/BeforeContainerConfigurationIsAppliedListenerTest.php new file mode 100644 index 0000000..bafa4cb --- /dev/null +++ b/Tests/Functional/EventListener/BeforeContainerConfigurationIsAppliedListenerTest.php @@ -0,0 +1,45 @@ +__invoke($event); + + $this->assertContains( + 'EXT:ew_collapsible_container/Resources/Private/Partials/', + $event->getContainerConfiguration()->getGridPartialPaths() + ); + } +} diff --git a/Tests/Functional/EventListener/BeforeContainerPreviewIsRenderedListenerTest.php b/Tests/Functional/EventListener/BeforeContainerPreviewIsRenderedListenerTest.php new file mode 100644 index 0000000..1d88c09 --- /dev/null +++ b/Tests/Functional/EventListener/BeforeContainerPreviewIsRenderedListenerTest.php @@ -0,0 +1,258 @@ +configureTCA(); + + $this->importCSVDataSet(__DIR__ . '/../../Fixtures/tt_content.csv'); + $this->importCSVDataSet(__DIR__ . '/../../Fixtures/be_users.csv'); + $backendUser = $this->setUpBackendUser(1); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + } + + protected function configureTCA(): void + { + $configuration = new ContainerConfiguration( + 'test-container', + 'CType.I.test-container', + 'CType.I.test-container-plus_wiz_description', + [ + [ + [ + 'name' => 'Elements', + 'colPos' => 200, + 'allowed' => [ + 'CType' => 'test-child', + ], + ] + ] + ] + ); + + $configuration->setGroup('ew_fischer'); + $configuration->setIcon('content-card-group'); + + $this->get(Registry::class)->configureContainer($configuration); + + $GLOBALS['TCA']['tt_content']['ctrl']['typeicon_classes']['test-container'] = 'content-card-group'; + } + + protected function getContentRecords(string $field, int $uid): array + { + /** @var QueryBuilder $queryBuilder */ + $queryBuilder = $this->get(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $queryBuilder->getRestrictions()->removeAll(); + return $queryBuilder + ->select('*') + ->from('tt_content') + ->where($queryBuilder->expr()->eq($field, $uid)) + ->executeQuery() + ->fetchAssociative(); + } + + protected function getBeforeContainerPreviewIsRenderedEvent(array $record): ?BeforeContainerPreviewIsRenderedEvent + { + $context = new PageLayoutContext( + [], + new BackendLayout('', '', []), + new Site('test', 1, []), + new DrawingConfiguration(), + $this->get(ServerRequestFactory::class)->createServerRequest('GET', '/'), + ); + $item = new GridColumnItem($context, (new GridColumn($context, [])), $record); + $grid = new Grid($context); + + $language = (int)$record['sys_language_uid']; + /** @var Database $database */ + $database = $this->get(Database::class); + $children = $database->fetchRecordsByParentAndLanguage((int)$record['uid'], $language); + $childRecordByColPosKey = []; + foreach ($children as $child) { + if (empty($childRecordByColPosKey[$child['colPos']])) { + $childRecordByColPosKey[$child['colPos']] = []; + } + $childRecordByColPosKey[$child['colPos']][] = $child; + } + + $container = GeneralUtility::makeInstance(Container::class, $record, $childRecordByColPosKey, $language); + + $containerGrid = $this->get(Registry::class)->getGrid($record['CType']); + foreach ($containerGrid as $cols) { + $rowObject = GeneralUtility::makeInstance(GridRow::class, $context); + foreach ($cols as $col) { + $columnObject = GeneralUtility::makeInstance( + ContainerGridColumn::class, + $context, + $col, + $container, + '', + false + ); + $rowObject->addColumn($columnObject); + if (isset($col['colPos'])) { + $records = $container->getChildrenByColPos($col['colPos']); + foreach ($records as $contentRecord) { + $columnItem = GeneralUtility::makeInstance( + ContainerGridColumnItem::class, + $context, + $columnObject, + $contentRecord, + $container, + '' + ); + $columnObject->addItem($columnItem); + } + } + } + $grid->addRow($rowObject); + } + + /** @var StandaloneView $view */ + $view = $this->getMockBuilder(StandaloneView::class) + ->disableOriginalConstructor() + ->getMock(); + + return new BeforeContainerPreviewIsRenderedEvent($container, $view, $grid, $item); + } + + #[Test] + public function getCountOfHiddenItems(): void + { + $containerRecord = $this->getContentRecords('tx_container_parent', 0); + $event = $this->getBeforeContainerPreviewIsRenderedEvent($containerRecord); + + $subject = new BeforeContainerPreviewIsRenderedListener($this->get(PageRenderer::class)); + $subject->__invoke($event); + + $definition = $event->getGrid()->getColumns()[200]->getDefinition(); + $this->assertEquals(1, $definition['countOfHiddenItems']); + } + + public static function getCollapsedProvider(): array + { + return [ + 'falseIsDefault' => [ false ], + 'trueIsDefault' => [ true ], + ]; + } + + #[Test] + #[DataProvider('getCollapsedProvider')] + public function getCollapsed(bool $state): void + { + $GLOBALS['TCA']['tt_content']['containerConfiguration']['test-container']['grid'][0][0]['collapsed'] = $state; + $containerRecord = $this->getContentRecords('tx_container_parent', 0); + $event = $this->getBeforeContainerPreviewIsRenderedEvent($containerRecord); + + $subject = new BeforeContainerPreviewIsRenderedListener($this->get(PageRenderer::class)); + $subject->__invoke($event); + + $definition = $event->getGrid()->getColumns()[200]->getDefinition(); + $this->assertEquals($state, $definition['collapsed']); + } + + public static function showMinItemsProvider(): array + { + return [ + 'minItemsIsHigherThenAvailableItems' => [3, true], + 'minItemsIsNotHigherThenAvailableItems' => [2, false], + ]; + } + + #[Test] + #[DataProvider('showMinItemsProvider')] + public function getShowMinItemsWarning(int $minitems, bool $expected): void + { + $GLOBALS['TCA']['tt_content']['containerConfiguration']['test-container']['grid'][0][0]['minitems'] = $minitems; + $containerRecord = $this->getContentRecords('tx_container_parent', 0); + $event = $this->getBeforeContainerPreviewIsRenderedEvent($containerRecord); + + $subject = new BeforeContainerPreviewIsRenderedListener($this->get(PageRenderer::class)); + $subject->__invoke($event); + + $definition = $event->getGrid()->getColumns()[200]->getDefinition(); + $this->assertEquals($expected, $definition['showMinItemsWarning']); + } + + #[Test] + public function addFrontendResourcesAddJavascriptAndStylesheets(): void + { + $containerRecord = $this->getContentRecords('tx_container_parent', 0); + $event = $this->getBeforeContainerPreviewIsRenderedEvent($containerRecord); + + /** @var PageRenderer $pageRenderer */ + $pageRenderer = $this->get(PageRenderer::class); + + $subject = new BeforeContainerPreviewIsRenderedListener($pageRenderer); + $subject->__invoke($event); + + $reflectedClass = new \ReflectionClass($pageRenderer); + $property = $reflectedClass->getProperty('cssFiles'); + + $this->assertArrayHasKey( + 'EXT:ew_collapsible_container/Resources/Public/Css/container.css', + $property->getValue($pageRenderer) + ); + + $moduleName = '@evoweb/ew-collapsible-container/container.js'; + $javascriptInstruction = array_map( + fn (array $item) => $item['payload']->getName() === $moduleName ? $moduleName : '', + $pageRenderer->getJavaScriptRenderer()->toArray() + ); + + $this->assertContains($moduleName, $javascriptInstruction); + } +} diff --git a/Tests/Functional/Xclass/ContainerGridColumnTest.php b/Tests/Functional/Xclass/ContainerGridColumnTest.php new file mode 100644 index 0000000..3eeec62 --- /dev/null +++ b/Tests/Functional/Xclass/ContainerGridColumnTest.php @@ -0,0 +1,66 @@ +importCSVDataSet(__DIR__ . '/../../Fixtures/be_users.csv'); + $backendUser = $this->setUpBackendUser(1); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $pageLayoutContext = new PageLayoutContext( + [], + new BackendLayout('', '', []), + new Site('test', 1, []), + new DrawingConfiguration(), + $this->get(ServerRequestFactory::class)->createServerRequest('GET', '/'), + ); + + $container = new Container([], [], 0); + + $subject = new ContainerGridColumn( + $pageLayoutContext, + [ + 'colPos' => 200, + ], + $container, + '', + false + ); + + $subject->setOverride([ + 'countOfHiddenItems' => 0, + 'collapsed' => false, + 'showMinItemsWarning' => false, + ]); + + $this->assertArrayHasKey('countOfHiddenItems', $subject->getDefinition()); + } +} diff --git a/composer.json b/composer.json index f866afd..441c00b 100644 --- a/composer.json +++ b/composer.json @@ -50,12 +50,6 @@ "rm .gitignore", "sed -i \"s/version' => '.*'/version' => '$(echo ${GITHUB_REF} | cut -d / -f 3)'/\" ext_emconf.php\n" ], - "post-install-cmd": [ - "ln -sf vendor/typo3/testing-framework/Resources/Core/Build/ Build/phpunit;" - ], - "post-update-cmd": [ - "@post-install-cmd" - ], "post-autoload-dump": [ "TYPO3\\TestingFramework\\Composer\\ExtensionTestEnvironment::prepare" ] @@ -64,5 +58,10 @@ "psr-4": { "Evoweb\\EwCollapsibleContainer\\": "Classes/" } + }, + "autoload-dev": { + "psr-4": { + "Evoweb\\EwCollapsibleContainer\\Tests\\": "Tests/" + } } } diff --git a/ext_emconf.php b/ext_emconf.php index da3faea..453ba56 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -8,7 +8,7 @@ 'author_email' => 'ew-collapsible-container@evoweb.de', 'author_company' => 'evoWeb', 'state' => 'stable', - 'version' => '2.1.0', + 'version' => '2.2.0', 'constraints' => [ 'depends' => [ 'typo3' => '13.4.0-13.4.99',