magento2-docker/dev/tests/integration/testsuite/Magento/Test/Integrity/LayoutTest.php

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;
}
}