feat: Divi-Fähigkeiten (DiviFeature, Requirement, ConstantRequirement) an Call, Mirror und Manifest; alle acht Adapter getaggt; Tests für Vollständigkeit und die gepinnte Zuordnung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 21:59:43 +02:00
co-authored by Claude Sonnet 5.5
parent e6e0f914ba
commit 392fe7c569
19 changed files with 552 additions and 48 deletions
@@ -8,6 +8,7 @@ use ET\Builder\Packages\GlobalData\GlobalData;
use ET\Builder\Packages\GlobalData\GlobalPreset;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
defined('ABSPATH') || exit;
@@ -34,7 +35,8 @@ class DesignSystemReader implements DiviAdapter
new Call(GlobalData::class . '::get_global_variables', 0),
new Call(GlobalPreset::class . '::get_data', 0),
],
keys: [self::OPTIONS]
keys: [self::OPTIONS],
features: [DiviFeature::DesignRead, DiviFeature::PageRead]
);
}
@@ -10,6 +10,7 @@ use ET\Builder\Packages\GlobalData\GlobalPreset;
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
@@ -44,7 +45,8 @@ class DesignSystemWriter implements DiviAdapter
return new Manifest(
Manifest::DESIGN,
calls: [
new Call(GlobalPreset::class . '::get_data', 0),
// Read by the preset tools as well as by the design system tool.
new Call(GlobalPreset::class . '::get_data', 0, [DiviFeature::DesignRead, DiviFeature::PresetWrite]),
new Call(GlobalPreset::class . '::prepare_data', 1),
new Call(GlobalPreset::class . '::validate_preset_count', 3),
new Call(GlobalPreset::class . '::get_deleted_preset_ids', 3),
@@ -52,8 +54,17 @@ class DesignSystemWriter implements DiviAdapter
new Call(GlobalPreset::class . '::save_data', 1),
new Call(Conversion::class . '::get_preset_attrs_mapping', 1),
new Call(Conversion::class . '::get_preset_attrs', 3),
new Call(GlobalPreset::class . '::get_group_preset_default_attr', 1),
new Call(ModuleRegistration::class . '::get_module_settings', 1),
// groupsOf(): also used by the composer for group presets (insert/update/assign), so Catalog too.
new Call(
GlobalPreset::class . '::get_group_preset_default_attr',
1,
[DiviFeature::Catalog, DiviFeature::PresetWrite]
),
new Call(
ModuleRegistration::class . '::get_module_settings',
1,
[DiviFeature::Catalog, DiviFeature::PresetWrite]
),
new Call(UserRole::class . '::can_current_user_use_visual_builder', 0),
],
mirrors: [
@@ -65,7 +76,8 @@ class DesignSystemWriter implements DiviAdapter
'Speicherablauf der Presets (prepare_data, validate_preset_count, Bereinigung der Divi-4-Option '
. 'beim Löschen, save_data)'
),
]
],
features: [DiviFeature::PresetWrite]
);
}
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Divi\Library;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
@@ -55,7 +56,14 @@ class DiviLibraryStore implements DiviAdapter
{
return new Manifest(
Manifest::LIBRARY,
calls: [new Call(self::GLOBAL_LAYOUT . '::is_global_layout_template', 1)],
calls: [
// isGlobal() serves listing and describing as well as the global edits.
new Call(
self::GLOBAL_LAYOUT . '::is_global_layout_template',
1,
[DiviFeature::LibraryRead, DiviFeature::LibraryWrite]
),
],
postTypes: [self::POST_TYPE],
taxonomies: [...self::TAXONOMIES, 'layout_category', 'layout_tag'],
keys: [self::MODULE_TYPE, self::EXCLUDED_OPTIONS, self::BUILT_FOR, self::TEMPLATE_TYPE],
@@ -68,7 +76,8 @@ class DiviLibraryStore implements DiviAdapter
'Terme und Meta neuer Bibliothekseinträge (scope, layout_type, module_width, _et_pb_module_type, '
. '_et_pb_excluded_global_options, _et_pb_built_for_post_type, _et_pb_template_type)'
),
]
],
features: [DiviFeature::LibraryWrite]
);
}
+6 -1
View File
@@ -8,6 +8,7 @@ use Closure;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use WP_Error;
@@ -33,7 +34,11 @@ class DiviRoles implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(Manifest::LIBRARY, calls: [new Call('et_pb_is_allowed', 1)]);
return new Manifest(
Manifest::LIBRARY,
calls: [new Call('et_pb_is_allowed', 1)],
features: [DiviFeature::Roles]
);
}
/** @param (Closure(string): bool)|null $check Test seam; null = Divi's et_pb_is_allowed(). */
+9 -2
View File
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
@@ -36,7 +37,12 @@ final class DiviPages implements DiviAdapter
return new Manifest(
Manifest::PAGES,
calls: [
new Call('ET_Core_PageResource::remove_static_resources', 3),
// Also called by DiviThemeBuilder::clearSiteCaches() – a break locks both features.
new Call(
'ET_Core_PageResource::remove_static_resources',
3,
[DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite]
),
new Call('et_builder_get_public_post_types', 0),
],
keys: ['_et_pb_use_divi_5', '_et_pb_use_builder', '_et_pb_show_page_creation', 'et_fb'],
@@ -48,7 +54,8 @@ final class DiviPages implements DiviAdapter
'SyncToServerController',
'Builder-Kennzeichen einer Seite (_et_pb_use_divi_5, _et_pb_use_builder, _et_pb_show_page_creation)'
),
]
],
features: [DiviFeature::PageWrite]
);
}
@@ -9,6 +9,7 @@ use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Log;
use Throwable;
@@ -46,7 +47,11 @@ class DynamicContentMap implements DiviAdapter
public static function manifest(): Manifest
{
return new Manifest(Manifest::CATALOG, calls: [new Call(self::REGISTRY . '::get_options', 2)]);
return new Manifest(
Manifest::CATALOG,
calls: [new Call(self::REGISTRY . '::get_options', 2)],
features: [DiviFeature::DynamicSources]
);
}
/** @return array<string, array<string, string>>|null Null = use DynamicContentRules' default map. */
+15 -4
View File
@@ -10,11 +10,17 @@ defined('ABSPATH') || exit;
* One Divi function or static method an adapter calls (spec §4.1), with the number of arguments the plugin passes.
* $args null: only its existence matters (a function Divi itself needs, e.g. et_() inside store_template).
*/
final class Call
final class Call implements Requirement
{
/** @param string $symbol "function_name" or "Fully\Qualified\Class::method". */
public function __construct(public readonly string $symbol, public readonly ?int $args = null)
{
/**
* @param string $symbol "function_name" or "Fully\Qualified\Class::method".
* @param list<DiviFeature> $features [] = the default of the manifest.
*/
public function __construct(
public readonly string $symbol,
public readonly ?int $args = null,
private readonly array $features = []
) {
}
public function className(): ?string
@@ -27,6 +33,11 @@ final class Call
return str_contains($this->symbol, '::') ? explode('::', $this->symbol, 2)[1] : $this->symbol;
}
public function features(): array
{
return $this->features;
}
public function label(): string
{
$class = $this->className();
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/** A Divi constant an adapter reads (spec §4.1); it must be defined. */
final class ConstantRequirement implements Requirement
{
/** @param list<DiviFeature> $features [] = the default of the manifest. */
public function __construct(public readonly string $name, private readonly array $features = [])
{
}
public function label(): string
{
return 'Konstante ' . $this->name;
}
public function features(): array
{
return $this->features;
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
// The installed PHPCompatibility sniffs predate enums (PHP 8.1) and read their methods as plain functions.
// phpcs:disable PHPCompatibility.Variables.ForbiddenThisUseContexts.OutsideObjectContext
/**
* What the plugin can do with Divi, as far as a Divi update may break it separately: every manifest entry (call,
* mirror, constant) belongs to one or more features, and the release gate decides per feature.
*/
enum DiviFeature: string
{
case PageRead = 'page-read';
case PageWrite = 'page-write';
case Catalog = 'catalog';
case DynamicSources = 'dynamic-sources';
case DesignRead = 'design-read';
case PresetWrite = 'preset-write';
case LibraryRead = 'library-read';
case LibraryWrite = 'library-write';
case Roles = 'roles';
case ThemeBuilderRead = 'tb-read';
case ThemeBuilderWrite = 'tb-write';
/** Whether the feature changes data (the four *Write cases). */
public function isWrite(): bool
{
return match ($this) {
self::PageWrite, self::PresetWrite, self::LibraryWrite, self::ThemeBuilderWrite => true,
default => false,
};
}
/** Name for messages, the status tool and the admin tab (German). */
public function label(): string
{
return match ($this) {
self::PageRead => 'Seiten lesen',
self::PageWrite => 'Seiten schreiben',
self::Catalog => 'Modulkatalog',
self::DynamicSources => 'Dynamische Inhalte',
self::DesignRead => 'Designsystem lesen',
self::PresetWrite => 'Presets schreiben',
self::LibraryRead => 'Bibliothek lesen',
self::LibraryWrite => 'Bibliothek schreiben',
self::Roles => 'Divi-Rollen',
self::ThemeBuilderRead => 'Theme Builder lesen',
self::ThemeBuilderWrite => 'Theme Builder schreiben',
};
}
}
+42 -12
View File
@@ -18,27 +18,56 @@ final class Manifest
public const DESIGN = 'Designsystem';
public const CATALOG = 'Katalog';
/** @var list<ConstantRequirement> */
public readonly array $constants;
/**
* @param string $area One of the constants above (the five areas of spec §3).
* @param list<Call> $calls Divi functions and static methods the adapter calls.
* @param list<string> $constants Divi constants it reads.
* @param list<string> $postTypes Divi post types it needs registered.
* @param list<string> $taxonomies Divi taxonomies it needs registered.
* @param list<string> $blockTypes Divi block types it needs (registered or in Divi's module metadata).
* @param list<string> $keys Other Divi names it uses as strings: meta keys, options, internal post
* types that need no registration check, query arguments.
* @param list<Mirror> $mirrors Divi functions whose storage logic it repeats.
* @param string $area One of the constants above (the five areas of spec §3).
* @param list<Call> $calls Divi functions and static methods the adapter calls.
* @param list<string|ConstantRequirement> $constants Divi constants it reads; a plain name stands for a
* ConstantRequirement without features of its own.
* @param list<string> $postTypes Divi post types it needs registered.
* @param list<string> $taxonomies Divi taxonomies it needs registered.
* @param list<string> $blockTypes Divi block types it needs (registered or in Divi's
* module metadata).
* @param list<string> $keys Other Divi names it uses as strings: meta keys, options,
* internal post types that need no registration check,
* query arguments.
* @param list<Mirror> $mirrors Divi functions whose storage logic it repeats.
* @param list<DiviFeature> $features The features of every call, mirror and constant that names
* none itself (featuresOf()).
*/
public function __construct(
public readonly string $area,
public readonly array $calls = [],
public readonly array $constants = [],
array $constants = [],
public readonly array $postTypes = [],
public readonly array $taxonomies = [],
public readonly array $blockTypes = [],
public readonly array $keys = [],
public readonly array $mirrors = []
public readonly array $mirrors = [],
public readonly array $features = []
) {
$this->constants = array_map(
static fn (string|ConstantRequirement $constant): ConstantRequirement => is_string($constant)
? new ConstantRequirement($constant)
: $constant,
array_values($constants)
);
}
/** @return list<Requirement> Calls, constants and mirrors – everything the release gate checks as code. */
public function requirements(): array
{
return [...$this->calls, ...$this->constants, ...$this->mirrors];
}
/** @return list<DiviFeature> The features $requirement names itself, else this manifest's default. */
public function featuresOf(Requirement $requirement): array
{
$own = $requirement->features();
return $own !== [] ? $own : $this->features;
}
/**
@@ -51,11 +80,12 @@ final class Manifest
foreach ($this->calls as $call) {
$names[] = $call->className() ?? $call->member();
}
$constants = array_map(static fn (ConstantRequirement $constant): string => $constant->name, $this->constants);
$mirrored = array_map(static fn (Mirror $mirror): string => $mirror->function, $this->mirrors);
return array_values(array_unique(array_merge(
$names,
$this->constants,
$constants,
$this->postTypes,
$this->taxonomies,
$this->keys,
@@ -57,7 +57,8 @@ class ManifestCheck
$checks[$manifest->area . ': ' . $call->label()] = static fn (): bool => self::accepts($call);
}
foreach ($manifest->constants as $constant) {
$checks[$manifest->area . ': Konstante ' . $constant] = static fn (): bool => defined($constant);
$checks[$manifest->area . ': ' . $constant->label()] =
static fn (): bool => defined($constant->name);
}
}
+16 -8
View File
@@ -10,25 +10,33 @@ defined('ABSPATH') || exit;
* A Divi function whose storage logic an adapter repeats instead of calling it (spec §4.1 "mirrors"). Its
* fingerprint (SourceFingerprint) must match the one in fingerprints.json, else Divi changed what we copy.
*/
final class Mirror
final class Mirror implements Requirement
{
/**
* @param string $id Key in fingerprints.json and name of tests/Fixtures/divi/reference/<id>.txt.
* @param string $file Divi file, relative to the Divi theme directory.
* @param string $function Function or method name.
* @param string|null $class Class of a method – the short name is enough (the file fixes where it
* lives, and a short name is no Divi name for DiviBoundaryTest); null for a function.
* @param string $what What the plugin repeats (German, for reports).
* @param string $id Key in fingerprints.json and name of tests/Fixtures/divi/reference/<id>.txt.
* @param string $file Divi file, relative to the Divi theme directory.
* @param string $function Function or method name.
* @param string|null $class Class of a method – the short name is enough (the file fixes where it
* lives, and a short name is no Divi name for DiviBoundaryTest); null for a
* function.
* @param string $what What the plugin repeats (German, for reports).
* @param list<DiviFeature> $features [] = the default of the manifest.
*/
public function __construct(
public readonly string $id,
public readonly string $file,
public readonly string $function,
public readonly ?string $class,
public readonly string $what
public readonly string $what,
private readonly array $features = []
) {
}
public function features(): array
{
return $this->features;
}
public function label(): string
{
$class = $this->class === null ? '' : SourceFingerprint::shortName($this->class) . '::';
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/** One thing an adapter needs from Divi (a call, a mirrored function or a constant) and the features it serves. */
interface Requirement
{
/** Name for reports, e.g. "GlobalPreset::get_data()" (without the manifest area). */
public function label(): string;
/**
* The features this requirement serves itself; [] = the default of the manifest that lists it
* (Manifest::featuresOf() resolves that).
*
* @return list<DiviFeature>
*/
public function features(): array;
}
+3 -1
View File
@@ -9,6 +9,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
@@ -46,7 +47,8 @@ class DiviModules implements DiviAdapter
],
constants: ['ET_BUILDER_PRODUCT_VERSION'],
blockTypes: [GlobalInstance::BLOCK],
keys: ['et_pb_canvas', '_et_pb_speculation']
keys: ['et_pb_canvas', '_et_pb_speculation'],
features: [DiviFeature::Catalog]
);
}
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
@@ -84,16 +85,31 @@ class DiviThemeBuilder implements DiviAdapter
return new Manifest(
Manifest::THEME_BUILDER,
calls: [
new Call('et_theme_builder_get_theme_builder_post_id', 2),
// liveId(): reads the live post, or creates it like Divi's first save.
new Call(
'et_theme_builder_get_theme_builder_post_id',
2,
[DiviFeature::ThemeBuilderRead, DiviFeature::ThemeBuilderWrite]
),
new Call('et_theme_builder_get_theme_builder_template_ids', 2),
new Call('et_theme_builder_get_template', 1),
new Call('et_theme_builder_insert_layout', 1),
new Call('et_theme_builder_store_template', 3),
new Call('et_theme_builder_insert_layout', 1, [DiviFeature::ThemeBuilderWrite]),
new Call('et_theme_builder_store_template', 3, [DiviFeature::ThemeBuilderWrite]),
new Call('et_theme_builder_get_flat_template_settings_options', 0),
new Call('et_theme_builder_clear_wp_cache', 1),
// clearSiteCaches() also runs after every page save with a site-wide scope (DiviSaver).
new Call(
'et_theme_builder_clear_wp_cache',
1,
[DiviFeature::ThemeBuilderWrite, DiviFeature::PageWrite]
),
// store_template needs Divi's et_() helper (E15).
new Call('et_'),
new Call('ET_Core_PageResource::remove_static_resources', 3),
new Call('et_', null, [DiviFeature::ThemeBuilderWrite]),
// Also called by DiviPages::clearPageCss() – a break locks both features.
new Call(
'ET_Core_PageResource::remove_static_resources',
3,
[DiviFeature::ThemeBuilderWrite, DiviFeature::PageWrite]
),
],
postTypes: self::postTypes(),
keys: [
@@ -106,23 +122,27 @@ class DiviThemeBuilder implements DiviAdapter
'et_theme_builder_api_save',
null,
'Vorlagenliste des Live-Theme-Builders (_et_template mit Sicherung et_tb_templates_backup_) und '
. '„ungenutzt“-Markierung'
. '„ungenutzt“-Markierung',
[DiviFeature::ThemeBuilderWrite]
),
new Mirror(
'tb-store-template',
$dir . 'theme-builder.php',
'et_theme_builder_store_template',
null,
'Bedingungen (_et_use_on, _et_exclude_from) und Aktiv-Schalter (_et_enabled) einer Vorlage'
'Bedingungen (_et_use_on, _et_exclude_from) und Aktiv-Schalter (_et_enabled) einer Vorlage',
[DiviFeature::ThemeBuilderWrite]
),
new Mirror(
'tb-load-setting-options',
$dir . 'theme-builder.php',
'et_theme_builder_load_template_setting_options',
null,
'Zerlegen der Bedingungs-IDs (TemplateConditions)'
'Zerlegen der Bedingungs-IDs (TemplateConditions)',
[DiviFeature::ThemeBuilderRead, DiviFeature::ThemeBuilderWrite]
),
]
],
features: [DiviFeature::ThemeBuilderRead]
);
}
@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use PHPUnit\Framework\TestCase;
/**
* Every manifest entry of every adapter names the Divi features it serves (D2 of the plan "Divi-Fähigkeiten"), and
* the assignment is pinned here: a changed tag shows up in review as a changed row of this table.
*/
final class DiviFeatureAssignmentTest extends TestCase
{
/** Adapter => requirement ("Nachbau " before a mirror's label) => features (sorted values). */
private const EXPECTED = [
DiviPages::class => [
'ET_Core_PageResource::remove_static_resources()' => ['page-write', 'tb-write'],
'et_builder_get_public_post_types()' => ['page-write'],
'Nachbau SyncToServerController::update()' => ['page-write'],
],
DiviLibraryStore::class => [
'GlobalLayout::is_global_layout_template()' => ['library-read', 'library-write'],
'Nachbau et_pb_submit_layout()' => ['library-write'],
],
DiviRoles::class => [
'et_pb_is_allowed()' => ['roles'],
],
DiviThemeBuilder::class => [
'et_theme_builder_get_theme_builder_post_id()' => ['tb-read', 'tb-write'],
'et_theme_builder_get_theme_builder_template_ids()' => ['tb-read'],
'et_theme_builder_get_template()' => ['tb-read'],
'et_theme_builder_insert_layout()' => ['tb-write'],
'et_theme_builder_store_template()' => ['tb-write'],
'et_theme_builder_get_flat_template_settings_options()' => ['tb-read'],
'et_theme_builder_clear_wp_cache()' => ['page-write', 'tb-write'],
'et_()' => ['tb-write'],
'ET_Core_PageResource::remove_static_resources()' => ['page-write', 'tb-write'],
'Nachbau et_theme_builder_api_save()' => ['tb-write'],
'Nachbau et_theme_builder_store_template()' => ['tb-write'],
'Nachbau et_theme_builder_load_template_setting_options()' => ['tb-read', 'tb-write'],
],
DesignSystemReader::class => [
'GlobalData::get_global_colors()' => ['design-read', 'page-read'],
'GlobalData::get_global_variables()' => ['design-read', 'page-read'],
'GlobalPreset::get_data()' => ['design-read', 'page-read'],
],
DesignSystemWriter::class => [
'GlobalPreset::get_data()' => ['design-read', 'preset-write'],
'GlobalPreset::prepare_data()' => ['preset-write'],
'GlobalPreset::validate_preset_count()' => ['preset-write'],
'GlobalPreset::get_deleted_preset_ids()' => ['preset-write'],
'GlobalPreset::remove_preset_from_legacy_option()' => ['preset-write'],
'GlobalPreset::save_data()' => ['preset-write'],
'Conversion::get_preset_attrs_mapping()' => ['preset-write'],
'Conversion::get_preset_attrs()' => ['preset-write'],
'GlobalPreset::get_group_preset_default_attr()' => ['catalog', 'preset-write'],
'ModuleRegistration::get_module_settings()' => ['catalog', 'preset-write'],
'UserRole::can_current_user_use_visual_builder()' => ['preset-write'],
'Nachbau GlobalPresetController::sync()' => ['preset-write'],
],
DiviModules::class => [
'ModuleRegistration::get_all_core_modules_metadata()' => ['catalog'],
'ModuleRegistration::get_default_attrs()' => ['catalog'],
'et_builder_enabled_for_post_type()' => ['catalog'],
'Konstante ET_BUILDER_PRODUCT_VERSION' => ['catalog'],
],
DynamicContentMap::class => [
'DynamicContentOptions::get_options()' => ['dynamic-sources'],
],
];
public function testEveryRequirementOfEveryAdapterServesAtLeastOneFeature(): void
{
$bare = [];
foreach (DiviAdapters::manifests() as $class => $manifest) {
foreach ($manifest->requirements() as $requirement) {
$features = $manifest->featuresOf($requirement);
if ($features === [] || array_unique($features, SORT_REGULAR) !== $features) {
$bare[] = $class . ': ' . $requirement->label();
}
}
}
self::assertSame([], $bare, "Anforderungen ohne (eindeutige) Fähigkeit:\n" . implode("\n", $bare));
}
public function testEveryFeatureIsServedByAtLeastOneRequirement(): void
{
$served = [];
foreach (DiviAdapters::manifests() as $manifest) {
foreach ($manifest->requirements() as $requirement) {
foreach ($manifest->featuresOf($requirement) as $feature) {
$served[$feature->value] = true;
}
}
}
$missing = array_values(array_diff(
array_map(static fn (DiviFeature $feature): string => $feature->value, DiviFeature::cases()),
array_keys($served)
));
self::assertSame([], $missing, 'Fähigkeiten ohne Anforderung in einem Manifest');
}
public function testTheAssignmentIsThePinnedTable(): void
{
$actual = [];
foreach (DiviAdapters::manifests() as $class => $manifest) {
foreach ($manifest->requirements() as $requirement) {
$values = array_map(
static fn (DiviFeature $feature): string => $feature->value,
$manifest->featuresOf($requirement)
);
sort($values);
$name = ($requirement instanceof Mirror ? 'Nachbau ' : '') . $requirement->label();
$actual[$class][$name] = $values;
}
}
self::assertSame(self::EXPECTED, $actual);
self::assertSame(array_keys(self::EXPECTED), DiviAdapters::CLASSES, 'all eight adapters, in report order');
}
public function testReadingAdaptersCarryNoWriteFeature(): void
{
$readingOnly = [DesignSystemReader::class, DiviRoles::class, DiviModules::class, DynamicContentMap::class];
foreach ($readingOnly as $class) {
$manifest = DiviAdapters::manifests()[$class];
foreach ($manifest->requirements() as $requirement) {
foreach ($manifest->featuresOf($requirement) as $feature) {
self::assertFalse($feature->isWrite(), $class . ': ' . $requirement->label());
}
}
}
}
public function testEveryMirrorServesAWriteFeature(): void
{
foreach (DiviAdapters::manifests() as $class => $manifest) {
foreach ($manifest->mirrors as $mirror) {
$writes = array_filter(
$manifest->featuresOf($mirror),
static fn (DiviFeature $feature): bool => $feature->isWrite()
);
self::assertNotSame([], $writes, $class . ': Nachbau ' . $mirror->id . ' dient keinem Schreib-Feature');
}
}
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use PHPUnit\Framework\TestCase;
final class DiviFeatureTest extends TestCase
{
public function testTheElevenFeaturesAndTheirValues(): void
{
self::assertSame(
[
'PageRead' => 'page-read',
'PageWrite' => 'page-write',
'Catalog' => 'catalog',
'DynamicSources' => 'dynamic-sources',
'DesignRead' => 'design-read',
'PresetWrite' => 'preset-write',
'LibraryRead' => 'library-read',
'LibraryWrite' => 'library-write',
'Roles' => 'roles',
'ThemeBuilderRead' => 'tb-read',
'ThemeBuilderWrite' => 'tb-write',
],
array_column(array_map(
static fn (DiviFeature $feature): array => [$feature->name, $feature->value],
DiviFeature::cases()
), 1, 0)
);
}
public function testOnlyTheFourWriteFeaturesWrite(): void
{
$writing = array_values(array_map(
static fn (DiviFeature $feature): string => $feature->value,
array_filter(DiviFeature::cases(), static fn (DiviFeature $feature): bool => $feature->isWrite())
));
self::assertSame(['page-write', 'preset-write', 'library-write', 'tb-write'], $writing);
}
public function testEveryFeatureHasAUniqueGermanLabel(): void
{
$labels = array_map(static fn (DiviFeature $feature): string => $feature->label(), DiviFeature::cases());
self::assertCount(count(DiviFeature::cases()), array_unique($labels));
self::assertSame('Seiten schreiben', DiviFeature::PageWrite->label());
self::assertSame('Theme Builder lesen', DiviFeature::ThemeBuilderRead->label());
foreach ($labels as $label) {
self::assertNotSame('', trim($label));
}
}
public function testValuesResolveBackToTheFeature(): void
{
self::assertSame(DiviFeature::ThemeBuilderWrite, DiviFeature::from('tb-write'));
self::assertNull(DiviFeature::tryFrom('page'));
}
}
@@ -5,6 +5,8 @@ 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\ConstantRequirement;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use PHPUnit\Framework\TestCase;
@@ -53,4 +55,61 @@ final class ManifestTest extends TestCase
);
self::assertSame('Bibliothek', $manifest->area);
}
public function testPlainConstantNamesBecomeRequirementsWithALabel(): void
{
$manifest = new Manifest(
Manifest::CATALOG,
constants: ['ET_BUILDER_PRODUCT_VERSION', new ConstantRequirement('ET_OTHER', [DiviFeature::Roles])]
);
self::assertSame(
['ET_BUILDER_PRODUCT_VERSION', 'ET_OTHER'],
array_map(static fn (ConstantRequirement $constant): string => $constant->name, $manifest->constants)
);
self::assertSame('Konstante ET_OTHER', $manifest->constants[1]->label());
self::assertSame([], $manifest->constants[0]->features());
self::assertSame([DiviFeature::Roles], $manifest->constants[1]->features());
self::assertSame(['ET_BUILDER_PRODUCT_VERSION', 'ET_OTHER'], $manifest->names());
}
public function testRequirementsAreCallsConstantsAndMirrorsInThatOrder(): void
{
$call = new Call('et_pb_is_allowed', 1);
$mirror = new Mirror('m', 'a.php', 'a', null, 'Terme');
$manifest = new Manifest(Manifest::LIBRARY, calls: [$call], constants: ['ET_X'], mirrors: [$mirror]);
$requirements = $manifest->requirements();
self::assertCount(3, $requirements);
self::assertSame($call, $requirements[0]);
self::assertSame('Konstante ET_X', $requirements[1]->label());
self::assertSame($mirror, $requirements[2]);
}
public function testFeaturesOfPrefersTheOwnFeaturesOverTheManifestDefault(): void
{
$own = new Call('et_a', null, [DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite]);
$inherited = new Call('et_b');
$mirror = new Mirror('m', 'a.php', 'a', null, 'x', [DiviFeature::LibraryWrite]);
$manifest = new Manifest(
Manifest::PAGES,
calls: [$own, $inherited],
constants: ['ET_X'],
mirrors: [$mirror],
features: [DiviFeature::PageRead]
);
self::assertSame([DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite], $manifest->featuresOf($own));
self::assertSame([DiviFeature::PageRead], $manifest->featuresOf($inherited));
self::assertSame([DiviFeature::PageRead], $manifest->featuresOf($manifest->constants[0]));
self::assertSame([DiviFeature::LibraryWrite], $manifest->featuresOf($mirror));
}
public function testWithoutAnyFeaturesAnEntryHasNone(): void
{
$call = new Call('et_a');
self::assertSame([], (new Manifest(Manifest::PAGES, calls: [$call]))->featuresOf($call));
}
}
@@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Schema;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\ConstantRequirement;
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
@@ -35,7 +36,10 @@ final class DiviModulesTest extends TestCase
array_map(static fn (Call $call): string => $call->label(), $manifest->calls)
);
self::assertSame(['divi/global-layout'], $manifest->blockTypes);
self::assertSame(['ET_BUILDER_PRODUCT_VERSION'], $manifest->constants);
self::assertSame(
['ET_BUILDER_PRODUCT_VERSION'],
array_map(static fn (ConstantRequirement $constant): string => $constant->name, $manifest->constants)
);
}
public function testInternalPostTypesAreDivisOwn(): void