338 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			338 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
		
			Executable File
		
	
	
<?php
 | 
						|
/**
 | 
						|
 * Layout nodes integrity tests
 | 
						|
 *
 | 
						|
 * Copyright © Magento, Inc. All rights reserved.
 | 
						|
 * See COPYING.txt for license details.
 | 
						|
 */
 | 
						|
 | 
						|
namespace Magento\Test\Integrity;
 | 
						|
 | 
						|
/**
 | 
						|
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 | 
						|
 */
 | 
						|
class LayoutTest extends \PHPUnit\Framework\TestCase
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * Cached lists of files
 | 
						|
     *
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    protected static $_cachedFiles = [];
 | 
						|
 | 
						|
    public static function setUpBeforeClass(): void
 | 
						|
    {
 | 
						|
        \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->configure(
 | 
						|
            ['preferences' => [\Magento\Theme\Model\Theme::class => \Magento\Theme\Model\Theme\Data::class]]
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    public static function tearDownAfterClass(): void
 | 
						|
    {
 | 
						|
        self::$_cachedFiles = []; // Free memory
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Composes full layout xml for designated parameters
 | 
						|
     *
 | 
						|
     * @param \Magento\Framework\View\Design\ThemeInterface $theme
 | 
						|
     * @return \Magento\Framework\View\Layout\Element
 | 
						|
     */
 | 
						|
    protected function _composeXml(\Magento\Framework\View\Design\ThemeInterface $theme)
 | 
						|
    {
 | 
						|
        /** @var \Magento\Framework\View\Layout\ProcessorInterface $layoutUpdate */
 | 
						|
        $layoutUpdate = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
 | 
						|
            \Magento\Framework\View\Layout\ProcessorInterface::class,
 | 
						|
            ['theme' => $theme]
 | 
						|
        );
 | 
						|
        return $layoutUpdate->getFileLayoutUpdatesXml();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Validate node's declared position in hierarchy and add errors to the specified array if found
 | 
						|
     *
 | 
						|
     * @param \SimpleXMLElement $node
 | 
						|
     * @param \Magento\Framework\View\Layout\Element $xml
 | 
						|
     * @param array &$errors
 | 
						|
     */
 | 
						|
    protected function _collectHierarchyErrors($node, $xml, &$errors)
 | 
						|
    {
 | 
						|
        $name = $node->getName();
 | 
						|
        $refName = $node->getAttribute('type') == $node->getAttribute('parent');
 | 
						|
        if ($refName) {
 | 
						|
            $refNode = $xml->xpath("/layouts/{$refName}");
 | 
						|
            if (!$refNode) {
 | 
						|
                $errors[$name][] = "Node '{$refName}', referenced in hierarchy, does not exist";
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * List all themes available in the system
 | 
						|
     *
 | 
						|
     * A test that uses such data provider is supposed to gather view resources in provided scope
 | 
						|
     * and analyze their integrity. For example, merge and verify all layouts in this scope.
 | 
						|
     *
 | 
						|
     * Such tests allow to uncover complicated code integrity issues, that may emerge due to view fallback mechanism.
 | 
						|
     * For example, a module layout file is overlapped by theme layout, which has mistakes.
 | 
						|
     * Such mistakes can be uncovered only when to emulate this particular theme.
 | 
						|
     * Also emulating "no theme" mode allows to detect inversed errors: when there is a view file with mistake
 | 
						|
     * in a module, but it is overlapped by every single theme by files without mistake. Putting question of code
 | 
						|
     * duplication aside, it is even more important to detect such errors, than an error in a single theme.
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function areasAndThemesDataProvider()
 | 
						|
    {
 | 
						|
        $result = [];
 | 
						|
        $themeCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
 | 
						|
            \Magento\Framework\View\Design\ThemeInterface::class
 | 
						|
        )->getCollection();
 | 
						|
        /** @var $theme \Magento\Framework\View\Design\ThemeInterface */
 | 
						|
        foreach ($themeCollection as $theme) {
 | 
						|
            $result[$theme->getFullPath() . ' [' . $theme->getId() . ']'] = [$theme];
 | 
						|
        }
 | 
						|
        return $result;
 | 
						|
    }
 | 
						|
 | 
						|
    public function testHandleLabels()
 | 
						|
    {
 | 
						|
        $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | 
						|
        $invoker(
 | 
						|
            /**
 | 
						|
             * @param \Magento\Framework\View\Design\ThemeInterface $theme
 | 
						|
             */
 | 
						|
            function (\Magento\Framework\View\Design\ThemeInterface $theme) {
 | 
						|
                $xml = $this->_composeXml($theme);
 | 
						|
 | 
						|
                $xpath = '/layouts/*[@design_abstraction]';
 | 
						|
                $handles = $xml->xpath($xpath) ?: [];
 | 
						|
 | 
						|
                /** @var \Magento\Framework\View\Layout\Element $node */
 | 
						|
                $errors = [];
 | 
						|
                foreach ($handles as $node) {
 | 
						|
                    if (!$node->xpath('@label')) {
 | 
						|
                        $nodeId = $node->getAttribute('id') ? ' id=' . $node->getAttribute('id') : '';
 | 
						|
                        $errors[] = $node->getName() . $nodeId;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                if ($errors) {
 | 
						|
                    $this->fail(
 | 
						|
                        'The following handles must have label, but they don\'t have it:' . PHP_EOL . var_export(
 | 
						|
                            $errors,
 | 
						|
                            true
 | 
						|
                        )
 | 
						|
                    );
 | 
						|
                }
 | 
						|
            },
 | 
						|
            $this->areasAndThemesDataProvider()
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    public function testPageTypesDeclaration()
 | 
						|
    {
 | 
						|
        $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | 
						|
        $invoker(
 | 
						|
            /**
 | 
						|
             * Check whether page types are declared only in layout update files allowed for it - base ones
 | 
						|
             */
 | 
						|
            function (\Magento\Framework\View\File $layout) {
 | 
						|
                $content = simplexml_load_file($layout->getFilename());
 | 
						|
                $this->assertEmpty(
 | 
						|
                    $content->xpath(\Magento\Framework\View\Model\Layout\Merge::XPATH_HANDLE_DECLARATION),
 | 
						|
                    "Theme layout update '" . $layout->getFilename() . "' contains page type declaration(s)"
 | 
						|
                );
 | 
						|
            },
 | 
						|
            $this->pageTypesDeclarationDataProvider()
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get theme layout updates
 | 
						|
     *
 | 
						|
     * @return \Magento\Framework\View\File[]
 | 
						|
     */
 | 
						|
    public function pageTypesDeclarationDataProvider()
 | 
						|
    {
 | 
						|
        /** @var $themeUpdates \Magento\Framework\View\File\Collector\ThemeModular */
 | 
						|
        $themeUpdates = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
 | 
						|
            ->create(\Magento\Framework\View\File\Collector\ThemeModular::class, ['subDir' => 'layout']);
 | 
						|
        /** @var $themeUpdatesOverride \Magento\Framework\View\File\Collector\Override\ThemeModular */
 | 
						|
        $themeUpdatesOverride = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
 | 
						|
            ->create(
 | 
						|
                \Magento\Framework\View\File\Collector\Override\ThemeModular::class,
 | 
						|
                ['subDir' => 'layout/override/theme']
 | 
						|
            );
 | 
						|
        /** @var $themeCollection \Magento\Theme\Model\Theme\Collection */
 | 
						|
        $themeCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
 | 
						|
            \Magento\Theme\Model\Theme\Collection::class
 | 
						|
        );
 | 
						|
        /** @var $themeLayouts \Magento\Framework\View\File[] */
 | 
						|
        $themeLayouts = [];
 | 
						|
        /** @var $theme \Magento\Framework\View\Design\ThemeInterface */
 | 
						|
        foreach ($themeCollection as $theme) {
 | 
						|
            $themeLayouts = array_merge($themeLayouts, $themeUpdates->getFiles($theme, '*.xml'));
 | 
						|
            $themeLayouts = array_merge($themeLayouts, $themeUpdatesOverride->getFiles($theme, '*.xml'));
 | 
						|
        }
 | 
						|
        $result = [];
 | 
						|
        foreach ($themeLayouts as $layout) {
 | 
						|
            $result[$layout->getFileIdentifier()] = [$layout];
 | 
						|
        }
 | 
						|
        return $result;
 | 
						|
    }
 | 
						|
 | 
						|
    public function testOverrideBaseFiles()
 | 
						|
    {
 | 
						|
        $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | 
						|
        $invoker(
 | 
						|
            /**
 | 
						|
             * Check, that for an overriding file ($themeFile) in a theme ($theme), there is a corresponding base file
 | 
						|
             *
 | 
						|
             * @param \Magento\Framework\View\File $themeFile
 | 
						|
             * @param \Magento\Framework\View\Design\ThemeInterface $theme
 | 
						|
             */
 | 
						|
            function ($themeFile, $theme) {
 | 
						|
                $baseFiles = self::_getCachedFiles(
 | 
						|
                    $theme->getArea(),
 | 
						|
                    \Magento\Framework\View\File\Collector\Base::class,
 | 
						|
                    $theme
 | 
						|
                );
 | 
						|
                $fileKey = $themeFile->getModule() . '/' . $themeFile->getName();
 | 
						|
                $this->assertArrayHasKey(
 | 
						|
                    $fileKey,
 | 
						|
                    $baseFiles,
 | 
						|
                    sprintf("Could not find base file, overridden by theme file '%s'.", $themeFile->getFilename())
 | 
						|
                );
 | 
						|
            },
 | 
						|
            $this->overrideBaseFilesDataProvider()
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    public function testOverrideThemeFiles()
 | 
						|
    {
 | 
						|
        $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | 
						|
        $invoker(
 | 
						|
            /**
 | 
						|
             * Check, that for an ancestor-overriding file ($themeFile) in a theme ($theme),
 | 
						|
             * there is a corresponding file in that ancestor theme
 | 
						|
             *
 | 
						|
             * @param \Magento\Framework\View\File $themeFile
 | 
						|
             * @param \Magento\Framework\View\Design\ThemeInterface $theme
 | 
						|
             */
 | 
						|
            function ($themeFile, $theme) {
 | 
						|
                // Find an ancestor theme, where a file is to be overridden
 | 
						|
                $ancestorTheme = $theme;
 | 
						|
                while ($ancestorTheme = $ancestorTheme->getParentTheme()) {
 | 
						|
                    if ($ancestorTheme == $themeFile->getTheme()) {
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                $this->assertNotNull(
 | 
						|
                    $ancestorTheme,
 | 
						|
                    sprintf(
 | 
						|
                        'Could not find ancestor theme "%s", ' .
 | 
						|
                        'its layout file is supposed to be overridden by file "%s".',
 | 
						|
                        $themeFile->getTheme()->getCode(),
 | 
						|
                        $themeFile->getFilename()
 | 
						|
                    )
 | 
						|
                );
 | 
						|
 | 
						|
                // Search for the overridden file in the ancestor theme
 | 
						|
                $ancestorFiles = self::_getCachedFiles(
 | 
						|
                    $ancestorTheme->getFullPath(),
 | 
						|
                    \Magento\Framework\View\File\Collector\ThemeModular::class,
 | 
						|
                    $ancestorTheme
 | 
						|
                );
 | 
						|
                $fileKey = $themeFile->getModule() . '/' . $themeFile->getName();
 | 
						|
                $this->assertArrayHasKey(
 | 
						|
                    $fileKey,
 | 
						|
                    $ancestorFiles,
 | 
						|
                    sprintf(
 | 
						|
                        "Could not find original file in '%s' theme, overridden by file '%s'.",
 | 
						|
                        $themeFile->getTheme()->getCode(),
 | 
						|
                        $themeFile->getFilename()
 | 
						|
                    )
 | 
						|
                );
 | 
						|
            },
 | 
						|
            $this->overrideThemeFilesDataProvider()
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Retrieve list of cached source files
 | 
						|
     *
 | 
						|
     * @param string $cacheKey
 | 
						|
     * @param string $sourceClass
 | 
						|
     * @param \Magento\Framework\View\Design\ThemeInterface $theme
 | 
						|
     * @return \Magento\Framework\View\File[]
 | 
						|
     */
 | 
						|
    protected static function _getCachedFiles(
 | 
						|
        $cacheKey,
 | 
						|
        $sourceClass,
 | 
						|
        \Magento\Framework\View\Design\ThemeInterface $theme
 | 
						|
    ) {
 | 
						|
        if (!isset(self::$_cachedFiles[$cacheKey])) {
 | 
						|
            /* @var $fileList \Magento\Framework\View\File[] */
 | 
						|
            $fileList = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
 | 
						|
                ->create($sourceClass, ['subDir' => 'layout'])->getFiles($theme, '*.xml');
 | 
						|
            $files = [];
 | 
						|
            foreach ($fileList as $file) {
 | 
						|
                $files[$file->getModule() . '/' . $file->getName()] = true;
 | 
						|
            }
 | 
						|
            self::$_cachedFiles[$cacheKey] = $files;
 | 
						|
        }
 | 
						|
        return self::$_cachedFiles[$cacheKey];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function overrideBaseFilesDataProvider()
 | 
						|
    {
 | 
						|
        return $this->_retrieveFilesForEveryTheme(
 | 
						|
            \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
 | 
						|
                ->create(
 | 
						|
                    \Magento\Framework\View\File\Collector\Override\Base::class,
 | 
						|
                    ['subDir' => 'layout/override/base']
 | 
						|
                )
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function overrideThemeFilesDataProvider()
 | 
						|
    {
 | 
						|
        return $this->_retrieveFilesForEveryTheme(
 | 
						|
            \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
 | 
						|
                ->create(
 | 
						|
                    \Magento\Framework\View\File\Collector\Override\ThemeModular::class,
 | 
						|
                    ['subDir' => 'layout/override/theme']
 | 
						|
                )
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Scan all the themes in the system, for each theme retrieve list of files via $filesRetriever,
 | 
						|
     * and return them as array of pairs [file, theme].
 | 
						|
     *
 | 
						|
     * @param \Magento\Framework\View\File\CollectorInterface $filesRetriever
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    protected function _retrieveFilesForEveryTheme(\Magento\Framework\View\File\CollectorInterface $filesRetriever)
 | 
						|
    {
 | 
						|
        $result = [];
 | 
						|
        /** @var $themeCollection \Magento\Theme\Model\Theme\Collection */
 | 
						|
        $themeCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
 | 
						|
            \Magento\Theme\Model\Theme\Collection::class
 | 
						|
        );
 | 
						|
        /** @var $theme \Magento\Framework\View\Design\ThemeInterface */
 | 
						|
        foreach ($themeCollection as $theme) {
 | 
						|
            foreach ($filesRetriever->getFiles($theme, '*.xml') as $file) {
 | 
						|
                $result['theme: ' . $theme->getFullPath() . ', ' . $file->getFilename()] = [$file, $theme];
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return $result;
 | 
						|
    }
 | 
						|
}
 |