feat: Divi manifests, adapter register and boundary test
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use FilesystemIterator;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviTouches;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* Spec §3: only the adapters in DiviAdapters::CLASSES touch Divi, and every Divi name an adapter uses stands in its
|
||||
* manifest (and nothing more).
|
||||
*/
|
||||
final class DiviBoundaryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Files that still touch Divi outside an adapter (paths below src/). Tasks 3–7 shrink this list to
|
||||
* DiviCompatibility.php; Task 8 removes it together with testLegacyFilesStillTouchDivi.
|
||||
*/
|
||||
private const LEGACY = [
|
||||
'Integrations/Divi/Design/DesignSystemReader.php',
|
||||
'Integrations/Divi/DiviCompatibility.php',
|
||||
'Integrations/Divi/Library/DiviLibraryStore.php',
|
||||
'Integrations/Divi/Library/DiviRoles.php',
|
||||
'Integrations/Divi/Library/LibraryItemSpec.php',
|
||||
'Integrations/Divi/Library/NewLibraryEntry.php',
|
||||
'Integrations/Divi/Pages/DiviFlags.php',
|
||||
'Integrations/Divi/Pages/DiviPage.php',
|
||||
'Integrations/Divi/Pages/DiviSaver.php',
|
||||
'Integrations/Divi/Pages/DynamicContentMap.php',
|
||||
'Integrations/Divi/Pages/WpLinkTargets.php',
|
||||
'Integrations/Divi/Schema/DiviModules.php',
|
||||
'Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php',
|
||||
'Integrations/Media/MediaUsageFinder.php',
|
||||
'Integrations/WordPress/Content/PostPresenter.php',
|
||||
'Integrations/WordPress/Schema/ContentSchema.php',
|
||||
];
|
||||
|
||||
public function testOnlyAdaptersTouchDivi(): void
|
||||
{
|
||||
$offenders = [];
|
||||
foreach (self::sourceFiles() as $relative => $path) {
|
||||
$touches = DiviTouches::inFile($path);
|
||||
if ($touches === [] || in_array($relative, self::LEGACY, true)) {
|
||||
continue;
|
||||
}
|
||||
if (! in_array(self::classOf($relative), DiviAdapters::CLASSES, true)) {
|
||||
$offenders[] = $relative . ': ' . implode(', ', array_unique(array_column($touches, 'name')));
|
||||
}
|
||||
}
|
||||
|
||||
self::assertSame([], $offenders, "Divi darf nur ein Adapter aus DiviAdapters::CLASSES berühren:\n"
|
||||
. implode("\n", $offenders));
|
||||
}
|
||||
|
||||
public function testLegacyFilesStillTouchDivi(): void
|
||||
{
|
||||
$files = self::sourceFiles();
|
||||
foreach (self::LEGACY as $relative) {
|
||||
self::assertArrayHasKey($relative, $files, $relative . ' gibt es nicht mehr – aus LEGACY streichen');
|
||||
self::assertNotSame([], DiviTouches::inFile($files[$relative]), $relative . ' ist sauber – aus LEGACY streichen');
|
||||
self::assertNotContains(self::classOf($relative), DiviAdapters::CLASSES, $relative . ' ist ein Adapter');
|
||||
}
|
||||
}
|
||||
|
||||
public function testAdaptersDeclareExactlyTheDiviNamesTheyUse(): void
|
||||
{
|
||||
foreach (DiviAdapters::CLASSES as $class) {
|
||||
self::assertTrue(is_subclass_of($class, DiviAdapter::class), $class . ' implementiert DiviAdapter nicht');
|
||||
$declared = $class::manifest()->names();
|
||||
$used = array_values(array_unique(array_column(
|
||||
DiviTouches::inFile((string) (new ReflectionClass($class))->getFileName()),
|
||||
'name'
|
||||
)));
|
||||
|
||||
self::assertSame([], array_values(array_diff($used, $declared)), $class . ': nicht im Manifest');
|
||||
self::assertSame(
|
||||
[],
|
||||
array_values(array_diff(array_filter($declared, [DiviTouches::class, 'detects']), $used)),
|
||||
$class . ': im Manifest, aber nicht benutzt'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMirrorIdsAreUniqueAndFilesRelative(): void
|
||||
{
|
||||
$ids = [];
|
||||
foreach (DiviAdapters::mirrors() as $mirror) {
|
||||
self::assertNotContains($mirror->id, $ids, 'doppelte Mirror-ID ' . $mirror->id);
|
||||
self::assertStringStartsNotWith('/', $mirror->file);
|
||||
$ids[] = $mirror->id;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, string> Path below src/ => absolute path. */
|
||||
private static function sourceFiles(): array
|
||||
{
|
||||
$root = dirname(__DIR__, 5) . '/src/';
|
||||
$files = [];
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->getExtension() === 'php') {
|
||||
$files[substr((string) $file->getPathname(), strlen($root))] = (string) $file->getPathname();
|
||||
}
|
||||
}
|
||||
ksort($files);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
private static function classOf(string $relative): string
|
||||
{
|
||||
return 'LIT\\MCP\\' . str_replace(['/', '.php'], ['\\', ''], $relative);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviTouches;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class DiviTouchesTest extends TestCase
|
||||
{
|
||||
public function testFindsCallsClassesConstantsAndNames(): void
|
||||
{
|
||||
$source = <<<'PHP'
|
||||
<?php
|
||||
use ET\Builder\Packages\GlobalData\GlobalData;
|
||||
// et_comment_only() and 'et_in_a_comment' are ignored.
|
||||
/** @see et_theme_builder_get_template() */
|
||||
final class X {
|
||||
public const REGISTRY = 'ET\Builder\Packages\Module\DynamicContentOptions';
|
||||
public function a(): void {
|
||||
et_pb_is_allowed('theme_builder');
|
||||
\et_theme_builder_clear_wp_cache('all');
|
||||
\ET_Core_PageResource::remove_static_resources(1, 'all', true);
|
||||
$v = defined('ET_BUILDER_PRODUCT_VERSION') ? ET_BUILDER_PRODUCT_VERSION : '';
|
||||
get_post_meta(1, '_et_pb_use_builder', true);
|
||||
function_exists('et_');
|
||||
$this->et_method();
|
||||
Foo::et_static();
|
||||
get_option('lit_mcp_limits');
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
$touches = array_map(
|
||||
static fn (array $t): string => $t['kind'] . ':' . $t['name'],
|
||||
DiviTouches::inSource($source)
|
||||
);
|
||||
|
||||
self::assertSame(
|
||||
[
|
||||
'class:ET\Builder\Packages\GlobalData\GlobalData',
|
||||
'class:ET\Builder\Packages\Module\DynamicContentOptions',
|
||||
'function:et_pb_is_allowed',
|
||||
'function:et_theme_builder_clear_wp_cache',
|
||||
'class:ET_Core_PageResource',
|
||||
'constant:ET_BUILDER_PRODUCT_VERSION',
|
||||
'constant:ET_BUILDER_PRODUCT_VERSION',
|
||||
'string:_et_pb_use_builder',
|
||||
'string:et_',
|
||||
],
|
||||
$touches
|
||||
);
|
||||
}
|
||||
|
||||
public function testDetectsOnlyNamesTheScannerReports(): void
|
||||
{
|
||||
self::assertTrue(DiviTouches::detects('et_pb_layout'));
|
||||
self::assertTrue(DiviTouches::detects('_et_template'));
|
||||
self::assertTrue(DiviTouches::detects('ET\Builder\Packages\GlobalData\GlobalData'));
|
||||
self::assertTrue(DiviTouches::detects('ET_Core_PageResource'));
|
||||
self::assertTrue(DiviTouches::detects('ET_BUILDER_PRODUCT_VERSION'));
|
||||
self::assertFalse(DiviTouches::detects('scope'));
|
||||
self::assertFalse(DiviTouches::detects('divi/global-layout'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ManifestTest extends TestCase
|
||||
{
|
||||
public function testCallsKnowTheirClassMemberAndLabel(): void
|
||||
{
|
||||
$function = new Call('et_pb_is_allowed', 1);
|
||||
$method = new Call('ET\Builder\Packages\GlobalData\GlobalData::get_global_colors', 0);
|
||||
|
||||
self::assertNull($function->className());
|
||||
self::assertSame('et_pb_is_allowed', $function->member());
|
||||
self::assertSame('et_pb_is_allowed()', $function->label());
|
||||
self::assertSame('ET\Builder\Packages\GlobalData\GlobalData', $method->className());
|
||||
self::assertSame('get_global_colors', $method->member());
|
||||
self::assertSame('GlobalData::get_global_colors()', $method->label());
|
||||
self::assertSame('ET_Core_PageResource', (new Call('\ET_Core_PageResource::remove_static_resources', 3))->className());
|
||||
}
|
||||
|
||||
public function testMirrorLabel(): void
|
||||
{
|
||||
$function = new Mirror('tb-api-save', 'includes/api.php', 'et_theme_builder_api_save', null, 'Vorlagenliste');
|
||||
$method = new Mirror('vb-flags', 'x.php', 'update', 'SyncToServerController', 'Kennzeichen');
|
||||
|
||||
self::assertSame('et_theme_builder_api_save()', $function->label());
|
||||
self::assertSame('SyncToServerController::update()', $method->label());
|
||||
}
|
||||
|
||||
public function testNamesListEveryDeclaredDiviName(): void
|
||||
{
|
||||
$manifest = new Manifest(
|
||||
Manifest::LIBRARY,
|
||||
calls: [new Call('et_pb_is_allowed', 1), new Call('ET\Builder\GlobalLayout::is_global_layout_template', 1)],
|
||||
constants: ['ET_BUILDER_PRODUCT_VERSION'],
|
||||
postTypes: ['et_pb_layout'],
|
||||
taxonomies: ['scope'],
|
||||
blockTypes: ['divi/global-layout'],
|
||||
keys: ['_et_pb_module_type']
|
||||
);
|
||||
|
||||
self::assertSame(
|
||||
['et_pb_is_allowed', 'ET\Builder\GlobalLayout', 'ET_BUILDER_PRODUCT_VERSION', 'et_pb_layout', 'scope',
|
||||
'_et_pb_module_type'],
|
||||
$manifest->names()
|
||||
);
|
||||
self::assertSame('Bibliothek', $manifest->area);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user