test: shared Divi fixtures in tests/Support (T1)

- DiviFixtures: markup(), givenPost(), schema(), designSystem(), catalog(),
  dynamicContentRegistry(), maliciousRefs() - moved verbatim out of
  DiviPageLoaderTest, DesignSystemTest, ModuleCatalogTest, DynamicContentMapTest
  and VariableRefTest (its malicious() provider now returns maliciousRefs()).
- ComposeFixture moves to tests/Support.
- DiviWriterFixture: modules(), designReader(), composition(), a recording saver(),
  log() and writer(&$saved, &$failures, unfiltered, loader) for the five tests that
  wired the Divi write stack by hand.
No test cases or assertions changed (1397 / 5834).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 17:47:11 +02:00
co-authored by Claude Opus 5.5
parent d2cc8497c9
commit 3d96a41366
24 changed files with 489 additions and 404 deletions
@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose; namespace LIT\MCP\Tests\Support;
use LIT\MCP\Integrations\Divi\Compose\AttrWriter; use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
use LIT\MCP\Integrations\Divi\Compose\Composer; use LIT\MCP\Integrations\Divi\Compose\Composer;
@@ -12,8 +12,6 @@ use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
use LIT\MCP\Integrations\Divi\Compose\StructureRules; use LIT\MCP\Integrations\Divi\Compose\StructureRules;
use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer; use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer;
use LIT\MCP\Integrations\Divi\Design\DesignSystem; use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
/** /**
* Compose classes wired with the real Divi 5.2.1 sample catalog, the sample design system and * Compose classes wired with the real Divi 5.2.1 sample catalog, the sample design system and
@@ -130,12 +128,12 @@ final class ComposeFixture
public static function placeholders(): Placeholders public static function placeholders(): Placeholders
{ {
return new Placeholders(DesignSystemTest::sample()); return new Placeholders(DiviFixtures::designSystem());
} }
public static function attrWriter(bool $unfiltered = false): AttrWriter public static function attrWriter(bool $unfiltered = false): AttrWriter
{ {
return new AttrWriter(ModuleCatalogTest::sampleCatalog(), self::placeholders(), self::sanitizer($unfiltered)); return new AttrWriter(DiviFixtures::catalog(), self::placeholders(), self::sanitizer($unfiltered));
} }
/** /**
@@ -149,7 +147,7 @@ final class ComposeFixture
public static function rules(): StructureRules public static function rules(): StructureRules
{ {
return new StructureRules(ModuleCatalogTest::sampleCatalog()); return new StructureRules(DiviFixtures::catalog());
} }
/** /**
@@ -158,12 +156,12 @@ final class ComposeFixture
*/ */
public static function composer(bool $unfiltered = false, ?DesignSystem $design = null): Composer public static function composer(bool $unfiltered = false, ?DesignSystem $design = null): Composer
{ {
$design ??= DesignSystemTest::sample(); $design ??= DiviFixtures::designSystem();
$placeholders = new Placeholders($design); $placeholders = new Placeholders($design);
$attrWriter = new AttrWriter(ModuleCatalogTest::sampleCatalog(), $placeholders, self::sanitizer($unfiltered)); $attrWriter = new AttrWriter(DiviFixtures::catalog(), $placeholders, self::sanitizer($unfiltered));
return new Composer( return new Composer(
ModuleCatalogTest::sampleCatalog(), DiviFixtures::catalog(),
self::rules(), self::rules(),
new ShortformWriter($placeholders, self::media(), $attrWriter), new ShortformWriter($placeholders, self::media(), $attrWriter),
$attrWriter, $attrWriter,
+231
View File
@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use Mockery;
use WP_Post;
/**
* Shared Divi test data: page markup, the stubbed post of the page tests, the sample design
* system and module catalog (real Divi 5.2.1 shapes), the dynamic content registry and the
* malicious $variable rows the Compose tests feed into every write path.
*/
final class DiviFixtures
{
/** A Divi page markup file from tests/Fixtures/divi. */
public static function markup(string $name): string
{
return (string) file_get_contents(dirname(__DIR__) . '/Fixtures/divi/' . $name);
}
/** Stubs everything PostGuard::editable() needs for a post with $content. */
public static function givenPost(string $content, array $fields = []): WP_Post
{
$post = new WP_Post(array_merge(
['ID' => 7, 'post_type' => 'page', 'post_status' => 'draft', 'post_title' => 'Start', 'post_content' => $content],
$fields
));
Functions\when('get_post')->justReturn($post);
Functions\when('current_user_can')->justReturn(true);
Functions\when('get_permalink')->justReturn('https://kunde.test/?page_id=7');
Functions\when('add_query_arg')->alias(static fn (string $key, string $value, string $url): string => $url . '&' . $key . '=' . $value);
return $post;
}
public static function schema(bool $diviEnabled = true): ContentSchema
{
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('isManaged')->andReturn(true);
$schema->allows('diviEnabled')->andReturn($diviEnabled);
return $schema;
}
/** Shapes as GlobalData / GlobalPreset return them (Divi 5.2.1, verified in DDEV). */
public static function designSystem(): DesignSystem
{
return DesignSystem::fromDivi(
[
'gcid-primary-color' => ['color' => '#2ea3f2', 'label' => 'Primärfarbe', 'status' => 'active'],
'gcid-body-color' => ['color' => '#666666', 'label' => 'Fließtextfarbe', 'status' => 'active'],
'gcid-a1b2' => ['color' => '#0b3d59', 'label' => 'Nachtblau', 'status' => 'active'],
'gcid-old' => ['color' => '#ff0000', 'label' => 'Alt', 'status' => 'inactive'],
],
[
'numbers' => ['gvid-r1' => ['id' => 'gvid-r1', 'label' => 'Radius', 'value' => '8px', 'status' => 'active']],
'strings' => ['gvid-s1' => ['id' => 'gvid-s1', 'label' => 'Telefon', 'value' => '0221 123', 'status' => 'active']],
// Divi returns every variable type as stdClass (verified in DDEV).
'fonts' => (object) [
'--et_global_heading_font' => ['id' => '--et_global_heading_font', 'label' => 'Überschrift', 'value' => 'Open Sans', 'status' => 'active'],
'--et_global_body_font' => ['id' => '--et_global_body_font', 'label' => 'Fließtext', 'value' => 'Lato', 'status' => 'active'],
],
'links' => new \stdClass(),
],
[
'module' => ['divi/button' => ['default' => 'p-1', 'items' => [
'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'],
'p-2' => ['id' => 'p-2', 'name' => 'CTA primär', 'moduleName' => 'divi/button'],
]]],
'group' => ['divi/font' => ['default' => '', 'items' => [
'g-1' => ['id' => 'g-1', 'name' => 'Fließtext groß', 'groupName' => 'divi/font'],
]]],
],
['body_font_size' => '16']
);
}
/** Real Divi 5.2.1 metadata (tests/Integration/divi-metadata-fixture.php). */
public static function catalog(): ModuleCatalog
{
$sample = json_decode(
(string) file_get_contents(dirname(__DIR__) . '/Fixtures/divi/metadata-sample.json'),
true
);
return new ModuleCatalog(
$sample['metadata'],
static fn (string $name): array => (array) ($sample['defaults'][$name] ?? [])
);
}
/**
* Shape of DynamicContentOptions::get_options($pageId, 'edit') in Divi 5.2.1 (excerpt of a
* live dump for a page: no post_categories/post_tags – pages have no such taxonomies).
*
* @return array<string, array<string, mixed>>
*/
public static function dynamicContentRegistry(): array
{
$text = static fn (): array => ['label' => 'x', 'type' => 'text', 'default' => ''];
$option = static fn (string $id, string $group, array $fields): array => [
'id' => $id, 'label' => $id, 'type' => 'text', 'custom' => false, 'group' => $group, 'fields' => $fields,
];
return [
'post_title' => $option('post_title', 'Default', ['before' => $text(), 'after' => $text()]),
'post_excerpt' => $option('post_excerpt', 'Default', [
'before' => $text(), 'after' => $text(), 'words' => $text(), 'read_more_label' => $text(),
]),
'post_date' => $option('post_date', 'Default', [
'before' => $text(),
'after' => $text(),
'date_format' => ['label' => 'x', 'type' => 'select', 'default' => 'default'],
'custom_date_format' => $text(),
]),
'post_author' => $option('post_author', 'Default', [
'before' => $text(),
'after' => $text(),
'name_format' => ['type' => 'select'],
'link' => ['type' => 'yes_no_button'],
]),
'site_title' => $option('site_title', 'Default', ['before' => $text(), 'after' => $text()]),
'post_link_url' => $option('post_link_url', 'Default', []),
'post_link_url_page' => $option('post_link_url_page', 'Default', ['post_id' => ['type' => 'select_post']]),
'post_featured_image' => $option('post_featured_image', 'Default', []),
'post_meta_key' => $option('post_meta_key', 'Standard', [
'before' => $text(),
'after' => $text(),
'select_meta_key' => ['type' => 'select'],
'meta_key' => $text(),
'enable_html' => ['type' => 'yes_no_button'],
]),
'loop_post_title' => $option('loop_post_title', 'Loop', [
'before' => $text(), 'after' => $text(), 'loop_position' => $text(),
]),
'loop_post_meta_key_manual_custom_field' => $option('loop_post_meta_key_manual_custom_field', 'Loop', [
'loop_meta_key' => $text(),
]),
// Odd entries a third-party filter could add.
'broken_fields' => $option('broken_fields', 'Default', ['a' => 'not-an-array', 'b' => ['type' => 3]]),
'no_fields' => ['id' => 'no_fields', 'type' => 'text'],
7 => $option('7', 'Default', []),
'not_an_array' => 'x',
];
}
/**
* Hand-crafted attacks (preflight §4 "Malicious $variable tests").
*
* @return array<string, array{string, string}> Value => part of the German error.
*/
public static function maliciousRefs(): array
{
$end = self::escape('0029') . '$';
return [
'escaped end marker in before' => [self::content('post_title', '{"before":"x' . $end . ' <b>y</b>","after":""}'), 'allein im Wert'],
'escaped end marker in after' => [self::content('post_title', '{"before":"","after":"' . $end . '"}'), 'allein im Wert'],
'escaped end marker in date format' => [self::content('post_date', '{"custom_date_format":"d' . $end . '"}'), 'allein im Wert'],
'escaped end marker in name' => [self::content('post_title' . $end, '{}'), 'Unbekannter Name'],
'select with markup' => [self::content('post_date', '{"date_format":"<b>d</b>"}'), 'date_format'],
'type evil' => ['$variable({"type":"evil","value":{"name":"gcid-a1b2","settings":{}}})$', 'Typ'],
'unknown gcid' => ['$variable({"type":"color","value":{"name":"gcid-nope","settings":{}}})$', 'gcid-nope'],
'unknown gvid' => [self::content('gvid-nope', '{}'), 'gvid-nope'],
'content gvid as color' => ['$variable({"type":"color","value":{"name":"gvid-s1","settings":{}}})$', 'gvid-s1'],
'color gcid as content' => [self::content('gcid-a1b2', '{}'), 'gcid-a1b2'],
'name with paren' => ['$variable({"type":"color","value":{"name":"gcid-a1b2)","settings":{}}})$', 'Name'],
'name with style breakout' => [
'$variable({"type":"color","value":{"name":"gcid-a1b2;}</style><script>alert(1)</script>","settings":{}}})$',
'Name',
],
'settings on a global variable' => [self::content('gvid-s1', '{"before":"<b>x</b>"}'), 'before'],
'settings on a theme font' => [self::content('--et_global_heading_font', '{"after":""}'), 'after'],
'unknown settings key' => [self::content('post_title', '{"before":"","foo":"1"}'), 'foo'],
'setting the option lacks' => [self::content('post_featured_image', '{"before":"x"}'), 'before'],
'enable_html' => [self::content('custom_meta_telefon', '{"before":"","after":"","enable_html":"on"}'), 'enable_html'],
'color setting not numeric' => [
'$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{"opacity":"1;}</style>"}}})$',
'opacity',
],
'protected meta key' => [self::content('custom_meta__edit_lock', '{}'), 'custom_meta__edit_lock'],
'doubled meta prefix' => [
self::content('custom_meta_custom_meta__edit_lock', '{}'),
'custom_meta_custom_meta__edit_lock',
],
'loop content outside a loop' => [self::content('loop_post_title', '{}'), 'loop_post_title'],
'unknown dynamic content' => [self::content('post_meta_key', '{}'), 'post_meta_key'],
'extra top-level key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}},"x":1})$', 'x'],
'extra value key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{},"x":1}})$', 'x'],
'greedy smuggling' => ['$variable({"a":"})$<script>alert(1)</script>"})$', 'allein im Wert'],
'nested reference in before' => [self::content('post_title', '{"before":"$variable(x)$","after":""}'), 'allein im Wert'],
// S28: a marker the sanitizer rebuilds inside a cleaned setting (html(), text()).
'tag-split marker in before' => [self::content('post_title', '{"before":"$variable<x>(y","after":""}'), 'allein im Wert'],
'NUL-split marker in after' => [
self::content('post_title', '{"before":"","after":"$variable' . self::escape('0000') . '(y"}'),
'allein im Wert',
],
'octet-split marker in date format' => [self::content('post_date', '{"custom_date_format":"$variable%41(y"}'), 'allein im Wert'],
// S32: text() keeps control characters (like sanitize_text_field); VariableRef strips them.
'NUL-split marker in date format' => [
self::content('post_date', '{"custom_date_format":"$variable' . self::escape('0000') . '(y"}'),
'allein im Wert',
],
'DEL-split marker in date format' => [
self::content('post_date', '{"custom_date_format":"$variable' . self::escape('007f') . '(y"}'),
'allein im Wert',
],
'nested marker in before' => [self::content('post_title', '{"before":"$variable(","after":""}'), 'allein im Wert'],
'broken json' => ['$variable(kaputt)$', 'JSON'],
'trailing text' => [self::content('post_title', '{}') . ' ', 'allein im Wert'],
'embedded in text' => ['<p>Hi ' . self::content('post_title', '{"before":"","after":""}') . '</p>', 'allein im Wert'],
];
}
private static function content(string $name, string $settings): string
{
return '$variable({"type":"content","value":{"name":"' . $name . '","settings":' . $settings . '}})$';
}
/** A JSON "\uXXXX" escape, built from bytes so no tool turns it into the character itself. */
private static function escape(string $hex): string
{
return chr(92) . 'u' . $hex;
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use Mockery;
use Throwable;
use WP_Post;
/**
* The Divi write stack of the page tests: DiviModules and DesignSystemReader serve the sample
* catalog (builder 5.2.1) and design system, DiviComposition uses ComposeFixture's sanitizer and
* media, and the saver records what it saves instead of touching the database.
*/
final class DiviWriterFixture
{
public static function modules(): DiviModules
{
$modules = Mockery::mock(DiviModules::class);
$modules->allows('catalog')->andReturn(DiviFixtures::catalog());
$modules->allows('builderVersion')->andReturn('5.2.1');
return $modules;
}
public static function designReader(): DesignSystemReader
{
$design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DiviFixtures::designSystem());
return $design;
}
/**
* $design is the reader the writer gets too (one instance, like the live wiring); without
* $registry the composition uses DynamicContentMap's default (VariableRef's built-in map in
* unit tests, where Divi's classes are missing).
*/
public static function composition(
DesignSystemReader $design,
bool $unfiltered = false,
?DynamicContentMap $registry = null
): DiviComposition {
return new DiviComposition(
self::modules(),
$design,
ComposeFixture::sanitizer($unfiltered),
ComposeFixture::media(),
$registry ?? new DynamicContentMap()
);
}
/**
* A DiviSaver whose save() appends the content to $saved, puts it on the post and reports
* revision 811. Each call first takes the next entry of $failures and throws it, if any.
*
* @param list<string> $saved
* @param list<Throwable> $failures
*/
public static function saver(array &$saved, array &$failures = []): DiviSaver
{
$saver = Mockery::mock(DiviSaver::class);
$saver->allows('save')->andReturnUsing(static function (WP_Post $post, string $content) use (&$saved, &$failures): array {
$failure = array_shift($failures);
if ($failure !== null) {
throw $failure;
}
$saved[] = $content;
$post->post_content = $content;
return ['revision_id' => 811, 'warnings' => []];
});
return $saver;
}
/** A ChangeLog that accepts every record (log id 4711). */
public static function log(): ChangeLog
{
$log = Mockery::mock(ChangeLog::class);
$log->allows('record')->andReturn(4711);
return $log;
}
/**
* The writer over the page of DiviFixtures::givenPost(), saving into $saved (see saver()).
*
* @param list<string> $saved
* @param list<Throwable> $failures
*/
public static function writer(
array &$saved,
array &$failures = [],
bool $unfiltered = false,
?DiviPageLoader $loader = null
): DiviWriter {
$design = self::designReader();
return new DiviWriter(
$loader ?? new DiviPageLoader(DiviFixtures::schema()),
self::saver($saved, $failures),
self::log(),
$design,
self::composition($design, $unfiltered)
);
}
}
@@ -6,7 +6,8 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
use InvalidArgumentException; use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Compose\AttrWriter; use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest; use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class AttrWriterTest extends TestCase final class AttrWriterTest extends TestCase
@@ -156,7 +157,7 @@ final class AttrWriterTest extends TestCase
public static function malicious(): array public static function malicious(): array
{ {
$cases = []; $cases = [];
foreach (VariableRefTest::malicious() as $name => [$value]) { foreach (DiviFixtures::maliciousRefs() as $name => [$value]) {
$cases[$name] = ['divi/text', 'content.innerContent', ['desktop' => $value], self::TEXT]; $cases[$name] = ['divi/text', 'content.innerContent', ['desktop' => $value], self::TEXT];
} }
@@ -317,7 +318,7 @@ final class AttrWriterTest extends TestCase
/** Ruling S23: content.innerContent of unknown kind (defined only in Divi's JS) is rich text. */ /** Ruling S23: content.innerContent of unknown kind (defined only in Divi's JS) is rich text. */
public function testUnknownContentInnerContentIsRichText(): void public function testUnknownContentInnerContentIsRichText(): void
{ {
self::assertSame('unknown', ModuleCatalogTest::sampleCatalog()->pathKind('divi/blurb', 'content.innerContent')); self::assertSame('unknown', DiviFixtures::catalog()->pathKind('divi/blurb', 'content.innerContent'));
self::assertSame('<p>Hallo</p>', ComposeFixture::attrWriter()->cleanValue('divi/blurb', 'content.innerContent', '<p>Hallo</p><script>x()</script>')); self::assertSame('<p>Hallo</p>', ComposeFixture::attrWriter()->cleanValue('divi/blurb', 'content.innerContent', '<p>Hallo</p><script>x()</script>'));
// Other unknown innerContent stays text. // Other unknown innerContent stays text.
self::assertSame('Los', ComposeFixture::attrWriter()->cleanValue('divi/button', 'button.innerContent', '<b>Los</b>')); self::assertSame('Los', ComposeFixture::attrWriter()->cleanValue('divi/button', 'button.innerContent', '<b>Los</b>'));
@@ -11,8 +11,8 @@ use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\BlockTree\Serializer; use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
use LIT\MCP\Integrations\Divi\Compose\Composer; use LIT\MCP\Integrations\Divi\Compose\Composer;
use LIT\MCP\Integrations\Divi\Design\DesignSystem; use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest; use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest; use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class ComposerTest extends TestCase final class ComposerTest extends TestCase
@@ -442,11 +442,11 @@ final class ComposerTest extends TestCase
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
new Composer( new Composer(
ModuleCatalogTest::sampleCatalog(), DiviFixtures::catalog(),
ComposeFixture::rules(), ComposeFixture::rules(),
ComposeFixture::shortform(), ComposeFixture::shortform(),
ComposeFixture::attrWriter(), ComposeFixture::attrWriter(),
DesignSystemTest::sample(), DiviFixtures::designSystem(),
'5.2.1"},"x":{"' '5.2.1"},"x":{"'
); );
} }
@@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
use InvalidArgumentException; use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Compose\Placeholders; use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Design\DesignSystem; use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Tests\Support\ComposeFixture;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class PlaceholdersTest extends TestCase final class PlaceholdersTest extends TestCase
@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose; namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
use InvalidArgumentException; use InvalidArgumentException;
use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class ShortformWriterTest extends TestCase final class ShortformWriterTest extends TestCase
@@ -146,7 +148,7 @@ final class ShortformWriterTest extends TestCase
public static function malicious(): array public static function malicious(): array
{ {
$rows = []; $rows = [];
foreach (VariableRefTest::malicious() as $name => [$value]) { foreach (DiviFixtures::maliciousRefs() as $name => [$value]) {
foreach (array_keys(self::TARGETS) as $kind) { foreach (array_keys(self::TARGETS) as $kind) {
$rows[$kind . ': ' . $name] = [$kind, $value, $kind !== 'text']; $rows[$kind . ': ' . $name] = [$kind, $value, $kind !== 'text'];
} }
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose; namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
use LIT\MCP\Integrations\Divi\BlockTree\Block; use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Tests\Support\ComposeFixture;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class StructureRulesTest extends TestCase final class StructureRulesTest extends TestCase
@@ -10,6 +10,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\Parser; use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\BlockTree\Serializer; use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
use LIT\MCP\Integrations\Divi\Compose\TreeEditor; use LIT\MCP\Integrations\Divi\Compose\TreeEditor;
use LIT\MCP\Tests\Support\ComposeFixture;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class TreeEditorTest extends TestCase final class TreeEditorTest extends TestCase
@@ -6,7 +6,8 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
use InvalidArgumentException; use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Compose\VariableRef; use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest; use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** Ruling S1/S2/S12/S21/S22: every $variable(…)$ value is parsed, strictly checked, cleaned and re-serialized. */ /** Ruling S1/S2/S12/S21/S22: every $variable(…)$ value is parsed, strictly checked, cleaned and re-serialized. */
@@ -15,7 +16,7 @@ final class VariableRefTest extends TestCase
/** @param array<string, array<string, string>>|null $dynamicContent */ /** @param array<string, array<string, string>>|null $dynamicContent */
private static function ref(?array $dynamicContent = null): VariableRef private static function ref(?array $dynamicContent = null): VariableRef
{ {
return new VariableRef(DesignSystemTest::sample(), $dynamicContent); return new VariableRef(DiviFixtures::designSystem(), $dynamicContent);
} }
private static function content(string $name, string $settings): string private static function content(string $name, string $settings): string
@@ -125,65 +126,7 @@ final class VariableRefTest extends TestCase
*/ */
public static function malicious(): array public static function malicious(): array
{ {
$end = self::escape('0029') . '$'; return DiviFixtures::maliciousRefs();
return [
'escaped end marker in before' => [self::content('post_title', '{"before":"x' . $end . ' <b>y</b>","after":""}'), 'allein im Wert'],
'escaped end marker in after' => [self::content('post_title', '{"before":"","after":"' . $end . '"}'), 'allein im Wert'],
'escaped end marker in date format' => [self::content('post_date', '{"custom_date_format":"d' . $end . '"}'), 'allein im Wert'],
'escaped end marker in name' => [self::content('post_title' . $end, '{}'), 'Unbekannter Name'],
'select with markup' => [self::content('post_date', '{"date_format":"<b>d</b>"}'), 'date_format'],
'type evil' => ['$variable({"type":"evil","value":{"name":"gcid-a1b2","settings":{}}})$', 'Typ'],
'unknown gcid' => ['$variable({"type":"color","value":{"name":"gcid-nope","settings":{}}})$', 'gcid-nope'],
'unknown gvid' => [self::content('gvid-nope', '{}'), 'gvid-nope'],
'content gvid as color' => ['$variable({"type":"color","value":{"name":"gvid-s1","settings":{}}})$', 'gvid-s1'],
'color gcid as content' => [self::content('gcid-a1b2', '{}'), 'gcid-a1b2'],
'name with paren' => ['$variable({"type":"color","value":{"name":"gcid-a1b2)","settings":{}}})$', 'Name'],
'name with style breakout' => [
'$variable({"type":"color","value":{"name":"gcid-a1b2;}</style><script>alert(1)</script>","settings":{}}})$',
'Name',
],
'settings on a global variable' => [self::content('gvid-s1', '{"before":"<b>x</b>"}'), 'before'],
'settings on a theme font' => [self::content('--et_global_heading_font', '{"after":""}'), 'after'],
'unknown settings key' => [self::content('post_title', '{"before":"","foo":"1"}'), 'foo'],
'setting the option lacks' => [self::content('post_featured_image', '{"before":"x"}'), 'before'],
'enable_html' => [self::content('custom_meta_telefon', '{"before":"","after":"","enable_html":"on"}'), 'enable_html'],
'color setting not numeric' => [
'$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{"opacity":"1;}</style>"}}})$',
'opacity',
],
'protected meta key' => [self::content('custom_meta__edit_lock', '{}'), 'custom_meta__edit_lock'],
'doubled meta prefix' => [
self::content('custom_meta_custom_meta__edit_lock', '{}'),
'custom_meta_custom_meta__edit_lock',
],
'loop content outside a loop' => [self::content('loop_post_title', '{}'), 'loop_post_title'],
'unknown dynamic content' => [self::content('post_meta_key', '{}'), 'post_meta_key'],
'extra top-level key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}},"x":1})$', 'x'],
'extra value key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{},"x":1}})$', 'x'],
'greedy smuggling' => ['$variable({"a":"})$<script>alert(1)</script>"})$', 'allein im Wert'],
'nested reference in before' => [self::content('post_title', '{"before":"$variable(x)$","after":""}'), 'allein im Wert'],
// S28: a marker the sanitizer rebuilds inside a cleaned setting (html(), text()).
'tag-split marker in before' => [self::content('post_title', '{"before":"$variable<x>(y","after":""}'), 'allein im Wert'],
'NUL-split marker in after' => [
self::content('post_title', '{"before":"","after":"$variable' . self::escape('0000') . '(y"}'),
'allein im Wert',
],
'octet-split marker in date format' => [self::content('post_date', '{"custom_date_format":"$variable%41(y"}'), 'allein im Wert'],
// S32: text() keeps control characters (like sanitize_text_field); VariableRef strips them.
'NUL-split marker in date format' => [
self::content('post_date', '{"custom_date_format":"$variable' . self::escape('0000') . '(y"}'),
'allein im Wert',
],
'DEL-split marker in date format' => [
self::content('post_date', '{"custom_date_format":"$variable' . self::escape('007f') . '(y"}'),
'allein im Wert',
],
'nested marker in before' => [self::content('post_title', '{"before":"$variable(","after":""}'), 'allein im Wert'],
'broken json' => ['$variable(kaputt)$', 'JSON'],
'trailing text' => [self::content('post_title', '{}') . ' ', 'allein im Wert'],
'embedded in text' => ['<p>Hi ' . self::content('post_title', '{"before":"","after":""}') . '</p>', 'allein im Wert'],
];
} }
/** @dataProvider malicious */ /** @dataProvider malicious */
@@ -7,7 +7,8 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
use LIT\MCP\Integrations\Divi\BlockTree\Document; use LIT\MCP\Integrations\Divi\BlockTree\Document;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex; use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\Compose\Warnings; use LIT\MCP\Integrations\Divi\Compose\Warnings;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest; use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class WarningsTest extends TestCase final class WarningsTest extends TestCase
@@ -34,7 +35,7 @@ final class WarningsTest extends TestCase
$document->append($block); $document->append($block);
} }
$warnings = (new Warnings(DesignSystemTest::sample()))->collect(NodeIndex::build($document), $composed['blocks']); $warnings = (new Warnings(DiviFixtures::designSystem()))->collect(NodeIndex::build($document), $composed['blocks']);
self::assertSame( self::assertSame(
[ [
@@ -50,6 +51,6 @@ final class WarningsTest extends TestCase
$warnings $warnings
); );
self::assertSame([], (new Warnings(DesignSystemTest::sample()))->collect(NodeIndex::build($document), [])); self::assertSame([], (new Warnings(DiviFixtures::designSystem()))->collect(NodeIndex::build($document), []));
} }
} }
@@ -5,46 +5,14 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design; namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use LIT\MCP\Integrations\Divi\Design\DesignSystem; use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class DesignSystemTest extends TestCase final class DesignSystemTest extends TestCase
{ {
/** Shapes as GlobalData / GlobalPreset return them (Divi 5.2.1, verified in DDEV). */
public static function sample(): DesignSystem
{
return DesignSystem::fromDivi(
[
'gcid-primary-color' => ['color' => '#2ea3f2', 'label' => 'Primärfarbe', 'status' => 'active'],
'gcid-body-color' => ['color' => '#666666', 'label' => 'Fließtextfarbe', 'status' => 'active'],
'gcid-a1b2' => ['color' => '#0b3d59', 'label' => 'Nachtblau', 'status' => 'active'],
'gcid-old' => ['color' => '#ff0000', 'label' => 'Alt', 'status' => 'inactive'],
],
[
'numbers' => ['gvid-r1' => ['id' => 'gvid-r1', 'label' => 'Radius', 'value' => '8px', 'status' => 'active']],
'strings' => ['gvid-s1' => ['id' => 'gvid-s1', 'label' => 'Telefon', 'value' => '0221 123', 'status' => 'active']],
// Divi returns every variable type as stdClass (verified in DDEV).
'fonts' => (object) [
'--et_global_heading_font' => ['id' => '--et_global_heading_font', 'label' => 'Überschrift', 'value' => 'Open Sans', 'status' => 'active'],
'--et_global_body_font' => ['id' => '--et_global_body_font', 'label' => 'Fließtext', 'value' => 'Lato', 'status' => 'active'],
],
'links' => new \stdClass(),
],
[
'module' => ['divi/button' => ['default' => 'p-1', 'items' => [
'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'],
'p-2' => ['id' => 'p-2', 'name' => 'CTA primär', 'moduleName' => 'divi/button'],
]]],
'group' => ['divi/font' => ['default' => '', 'items' => [
'g-1' => ['id' => 'g-1', 'name' => 'Fließtext groß', 'groupName' => 'divi/font'],
]]],
],
['body_font_size' => '16']
);
}
public function testReadsColorsVariablesFontsSizesAndPresets(): void public function testReadsColorsVariablesFontsSizesAndPresets(): void
{ {
$design = self::sample(); $design = DiviFixtures::designSystem();
self::assertSame( self::assertSame(
['id' => 'gcid-a1b2', 'label' => 'Nachtblau', 'value' => '#0b3d59'], ['id' => 'gcid-a1b2', 'label' => 'Nachtblau', 'value' => '#0b3d59'],
@@ -68,7 +36,7 @@ final class DesignSystemTest extends TestCase
public function testLooksUpByAliasLabelOrId(): void public function testLooksUpByAliasLabelOrId(): void
{ {
$design = self::sample(); $design = DiviFixtures::designSystem();
self::assertSame('gcid-primary-color', $design->colorId('primary')); self::assertSame('gcid-primary-color', $design->colorId('primary'));
self::assertSame('gcid-a1b2', $design->colorId('nachtblau')); self::assertSame('gcid-a1b2', $design->colorId('nachtblau'));
@@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use Brain\Monkey\Functions; use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader; use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility; use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery; use Mockery;
@@ -15,7 +16,7 @@ final class DiviDesignSystemAbilityTest extends TestCase
public function testReturnsDesignSystemWithPlaceholders(): void public function testReturnsDesignSystemWithPlaceholders(): void
{ {
$reader = Mockery::mock(DesignSystemReader::class); $reader = Mockery::mock(DesignSystemReader::class);
$reader->allows('read')->andReturn(DesignSystemTest::sample()); $reader->allows('read')->andReturn(DiviFixtures::designSystem());
Functions\when('current_user_can')->justReturn(true); Functions\when('current_user_can')->justReturn(true);
$ability = new DiviDesignSystemAbility($reader); $ability = new DiviDesignSystemAbility($reader);
@@ -14,9 +14,8 @@ use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition; use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap; use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules; use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture; use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest; use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery; use Mockery;
@@ -28,14 +27,14 @@ final class DiviCompositionTest extends TestCase
private function composition(): DiviComposition private function composition(): DiviComposition
{ {
$modules = Mockery::mock(DiviModules::class); $modules = Mockery::mock(DiviModules::class);
$modules->allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog()); $modules->allows('catalog')->andReturn(DiviFixtures::catalog());
$modules->allows('builderVersion')->andReturn('5.2.1'); $modules->allows('builderVersion')->andReturn('5.2.1');
$design = Mockery::mock(DesignSystemReader::class); $design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DesignSystemTest::sample()); $design->allows('read')->andReturn(DiviFixtures::designSystem());
$registry = new DynamicContentMap(function (int $postId): array { $registry = new DynamicContentMap(function (int $postId): array {
$this->registryCalls[] = $postId; $this->registryCalls[] = $postId;
return DynamicContentMapTest::registry(); return DiviFixtures::dynamicContentRegistry();
}); });
return new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media(), $registry); return new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media(), $registry);
@@ -6,15 +6,11 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use LIT\MCP\Integrations\Divi\BlockTree\Parser; use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Compose\VariableRef; use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer; use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver; use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap; use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules; use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture; use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery; use Mockery;
use Mockery\MockInterface; use Mockery\MockInterface;
@@ -48,20 +44,16 @@ final class DiviContentComposerTest extends TestCase
*/ */
private function composer(bool $diviEnabled = true, ?callable $registry = null): DiviContentComposer private function composer(bool $diviEnabled = true, ?callable $registry = null): DiviContentComposer
{ {
$modules = Mockery::mock(DiviModules::class); $design = DiviWriterFixture::designReader();
$modules->allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog());
$modules->allows('builderVersion')->andReturn('5.2.1');
$design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DesignSystemTest::sample());
$map = new DynamicContentMap(function (int $postId) use ($registry): array { $map = new DynamicContentMap(function (int $postId) use ($registry): array {
$this->registryCalls[] = $postId; $this->registryCalls[] = $postId;
return $registry !== null ? $registry($postId) : DynamicContentMapTest::registry(); return $registry !== null ? $registry($postId) : DiviFixtures::dynamicContentRegistry();
}); });
return new DiviContentComposer( return new DiviContentComposer(
DiviPageLoaderTest::schema($diviEnabled), DiviFixtures::schema($diviEnabled),
new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media(), $map), DiviWriterFixture::composition($design, false, $map),
$this->saver, $this->saver,
$design $design
); );
@@ -124,7 +116,7 @@ final class DiviContentComposerTest extends TestCase
public function testWriteRefusesWhatTheNewEntrysMapDoesNotOffer(): void public function testWriteRefusesWhatTheNewEntrysMapDoesNotOffer(): void
{ {
$registry = static function (int $postId): array { $registry = static function (int $postId): array {
$options = DynamicContentMapTest::registry(); $options = DiviFixtures::dynamicContentRegistry();
if ($postId !== 0) { if ($postId !== 0) {
unset($options['site_title']); unset($options['site_title']);
} }
@@ -4,66 +4,34 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages; namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Pages\DiviPage; use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader; use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Error; use WP_Error;
use WP_Post;
final class DiviPageLoaderTest extends TestCase final class DiviPageLoaderTest extends TestCase
{ {
public static function fixture(string $name): string
{
return (string) file_get_contents(dirname(__DIR__, 4) . '/Fixtures/divi/' . $name);
}
/** Stubs everything PostGuard::editable() needs for a post with $content. */
public static function givenPost(string $content, array $fields = []): WP_Post
{
$post = new WP_Post(array_merge(
['ID' => 7, 'post_type' => 'page', 'post_status' => 'draft', 'post_title' => 'Start', 'post_content' => $content],
$fields
));
Functions\when('get_post')->justReturn($post);
Functions\when('current_user_can')->justReturn(true);
Functions\when('get_permalink')->justReturn('https://kunde.test/?page_id=7');
Functions\when('add_query_arg')->alias(static fn (string $key, string $value, string $url): string => $url . '&' . $key . '=' . $value);
return $post;
}
public static function schema(bool $diviEnabled = true): ContentSchema
{
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('isManaged')->andReturn(true);
$schema->allows('diviEnabled')->andReturn($diviEnabled);
return $schema;
}
public function testLoadsTreeIndexAndTreeRev(): void public function testLoadsTreeIndexAndTreeRev(): void
{ {
$content = self::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
self::givenPost($content); DiviFixtures::givenPost($content);
$page = (new DiviPageLoader(self::schema()))->load(7, true); $page = (new DiviPageLoader(DiviFixtures::schema()))->load(7, true);
self::assertInstanceOf(DiviPage::class, $page); self::assertInstanceOf(DiviPage::class, $page);
self::assertSame(substr(hash('sha256', $content), 0, 12), $page->treeRev()); self::assertSame(substr(hash('sha256', $content), 0, 12), $page->treeRev());
self::assertSame('divi/button', $page->index->find('s0.r0.c1.m2')?->name()); self::assertSame('divi/button', $page->index->find('s0.r0.c1.m2')?->name());
self::assertTrue($page->isDiviLayout()); self::assertTrue($page->isDiviLayout());
self::assertSame('https://kunde.test/?page_id=7&et_fb=1', $page->editUrl()); self::assertSame('https://kunde.test/?page_id=7&et_fb=1', $page->editUrl());
self::assertNull((new DiviPageLoader(self::schema()))->writable($page)); self::assertNull((new DiviPageLoader(DiviFixtures::schema()))->writable($page));
} }
public function testRefusesDivi4Content(): void public function testRefusesDivi4Content(): void
{ {
self::givenPost(self::fixture('divi4-shortcodes.html')); DiviFixtures::givenPost(DiviFixtures::markup('divi4-shortcodes.html'));
$result = (new DiviPageLoader(self::schema()))->load(7, false); $result = (new DiviPageLoader(DiviFixtures::schema()))->load(7, false);
self::assertInstanceOf(WP_Error::class, $result); self::assertInstanceOf(WP_Error::class, $result);
self::assertSame('lit_unsupported', $result->get_error_code()); self::assertSame('lit_unsupported', $result->get_error_code());
@@ -72,12 +40,12 @@ final class DiviPageLoaderTest extends TestCase
public function testRefusesMalformedMarkupAndLogsIt(): void public function testRefusesMalformedMarkupAndLogsIt(): void
{ {
self::givenPost('<!-- wp:divi/section --><!-- wp:divi/row -->'); DiviFixtures::givenPost('<!-- wp:divi/section --><!-- wp:divi/row -->');
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead. // error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
$log = (string) tempnam(sys_get_temp_dir(), 'lit-log'); $log = (string) tempnam(sys_get_temp_dir(), 'lit-log');
$previous = ini_set('error_log', $log); $previous = ini_set('error_log', $log);
try { try {
$result = (new DiviPageLoader(self::schema()))->load(7, false); $result = (new DiviPageLoader(DiviFixtures::schema()))->load(7, false);
} finally { } finally {
ini_set('error_log', (string) $previous); ini_set('error_log', (string) $previous);
} }
@@ -90,8 +58,8 @@ final class DiviPageLoaderTest extends TestCase
public function testTrashedEntriesCanBeReadButNotWritten(): void public function testTrashedEntriesCanBeReadButNotWritten(): void
{ {
self::givenPost(self::fixture('synthetic-basic.html'), ['post_status' => 'trash']); DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'), ['post_status' => 'trash']);
$loader = new DiviPageLoader(self::schema()); $loader = new DiviPageLoader(DiviFixtures::schema());
self::assertInstanceOf(DiviPage::class, $loader->load(7, false)); self::assertInstanceOf(DiviPage::class, $loader->load(7, false));
self::assertSame('lit_validation_failed', $loader->load(7, true)->get_error_code()); self::assertSame('lit_validation_failed', $loader->load(7, true)->get_error_code());
@@ -99,18 +67,18 @@ final class DiviPageLoaderTest extends TestCase
public function testWritableChecks(): void public function testWritableChecks(): void
{ {
$loader = new DiviPageLoader(self::schema(false)); $loader = new DiviPageLoader(DiviFixtures::schema(false));
self::givenPost(''); DiviFixtures::givenPost('');
$empty = $loader->load(7, true); $empty = $loader->load(7, true);
self::assertSame('lit_unsupported', $loader->writable($empty)->get_error_code()); self::assertSame('lit_unsupported', $loader->writable($empty)->get_error_code());
$loader = new DiviPageLoader(self::schema()); $loader = new DiviPageLoader(DiviFixtures::schema());
self::givenPost("<!-- wp:paragraph -->\n<p>Gutenberg</p>\n<!-- /wp:paragraph -->"); DiviFixtures::givenPost("<!-- wp:paragraph -->\n<p>Gutenberg</p>\n<!-- /wp:paragraph -->");
$gutenberg = $loader->load(7, true); $gutenberg = $loader->load(7, true);
self::assertFalse($gutenberg->isDiviLayout()); self::assertFalse($gutenberg->isDiviLayout());
self::assertStringContainsString('kein Divi-Layout', $loader->writable($gutenberg)->get_error_message()); self::assertStringContainsString('kein Divi-Layout', $loader->writable($gutenberg)->get_error_message());
self::givenPost('', ['post_status' => 'future', 'post_date_gmt' => '2020-01-01 00:00:00']); DiviFixtures::givenPost('', ['post_status' => 'future', 'post_date_gmt' => '2020-01-01 00:00:00']);
$missed = $loader->load(7, true); $missed = $loader->load(7, true);
self::assertTrue($missed->isDiviLayout()); self::assertTrue($missed->isDiviLayout());
self::assertSame('lit_validation_failed', $loader->writable($missed)->get_error_code()); self::assertSame('lit_validation_failed', $loader->writable($missed)->get_error_code());
@@ -8,7 +8,7 @@ use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviNodeGetAbility; use LIT\MCP\Integrations\Divi\Pages\DiviNodeGetAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviOutlineAbility; use LIT\MCP\Integrations\Divi\Pages\DiviOutlineAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader; use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest; use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery; use Mockery;
use stdClass; use stdClass;
@@ -18,16 +18,16 @@ final class DiviReadAbilitiesTest extends TestCase
private function design(): DesignSystemReader private function design(): DesignSystemReader
{ {
$reader = Mockery::mock(DesignSystemReader::class); $reader = Mockery::mock(DesignSystemReader::class);
$reader->allows('read')->andReturn(DesignSystemTest::sample()); $reader->allows('read')->andReturn(DiviFixtures::designSystem());
return $reader; return $reader;
} }
public function testOutlineListsNodesWithTreeRevAndPostRev(): void public function testOutlineListsNodesWithTreeRevAndPostRev(): void
{ {
$content = DiviPageLoaderTest::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
DiviPageLoaderTest::givenPost($content); DiviFixtures::givenPost($content);
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviPageLoaderTest::schema()), $this->design()); $ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
self::assertTrue($ability->permission(['post_id' => 7])); self::assertTrue($ability->permission(['post_id' => 7]));
$result = $ability->execute(['post_id' => 7]); $result = $ability->execute(['post_id' => 7]);
@@ -46,8 +46,8 @@ final class DiviReadAbilitiesTest extends TestCase
/** Ruling S18: the "limit" input caps children per parent; "truncated" reports the cut. */ /** Ruling S18: the "limit" input caps children per parent; "truncated" reports the cut. */
public function testOutlineBreadthLimitTruncatesAndReportsIt(): void public function testOutlineBreadthLimitTruncatesAndReportsIt(): void
{ {
DiviPageLoaderTest::givenPost(DiviPageLoaderTest::fixture('synthetic-basic.html')); DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviPageLoaderTest::schema()), $this->design()); $ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
$result = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c1', 'limit' => 2]); $result = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c1', 'limit' => 2]);
@@ -58,8 +58,8 @@ final class DiviReadAbilitiesTest extends TestCase
public function testOutlineOfASubtreeAndUnknownNode(): void public function testOutlineOfASubtreeAndUnknownNode(): void
{ {
DiviPageLoaderTest::givenPost(DiviPageLoaderTest::fixture('synthetic-basic.html')); DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviPageLoaderTest::schema()), $this->design()); $ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
$result = $ability->execute(['post_id' => 7, 'id' => 's1', 'depth' => 1]); $result = $ability->execute(['post_id' => 7, 'id' => 's1', 'depth' => 1]);
self::assertSame(['s1', 's1.r0'], array_column($result['nodes'], 'id')); self::assertSame(['s1', 's1.r0'], array_column($result['nodes'], 'id'));
@@ -71,8 +71,8 @@ final class DiviReadAbilitiesTest extends TestCase
public function testOutlineValidatesTheSubtreeId(): void public function testOutlineValidatesTheSubtreeId(): void
{ {
DiviPageLoaderTest::givenPost(DiviPageLoaderTest::fixture('synthetic-basic.html')); DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviPageLoaderTest::schema()), $this->design()); $ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
self::assertSame( self::assertSame(
'lit_validation_failed', 'lit_validation_failed',
@@ -82,11 +82,11 @@ final class DiviReadAbilitiesTest extends TestCase
public function testOutlineHintsForEmptyAndReadOnlyContent(): void public function testOutlineHintsForEmptyAndReadOnlyContent(): void
{ {
DiviPageLoaderTest::givenPost(''); DiviFixtures::givenPost('');
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviPageLoaderTest::schema()), $this->design()); $ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
self::assertStringContainsString('lit-divi-insert', $ability->execute(['post_id' => 7])['hints'][0]); self::assertStringContainsString('lit-divi-insert', $ability->execute(['post_id' => 7])['hints'][0]);
DiviPageLoaderTest::givenPost(DiviPageLoaderTest::fixture('synthetic-mixed.html')); DiviFixtures::givenPost(DiviFixtures::markup('synthetic-mixed.html'));
$hints = $ability->execute(['post_id' => 7])['hints']; $hints = $ability->execute(['post_id' => 7])['hints'];
self::assertStringContainsString('nur lesbar', $hints[0]); self::assertStringContainsString('nur lesbar', $hints[0]);
self::assertStringContainsString('nur als Ganzes', $hints[0]); self::assertStringContainsString('nur als Ganzes', $hints[0]);
@@ -94,8 +94,8 @@ final class DiviReadAbilitiesTest extends TestCase
public function testNodeGetReturnsFullFormShortformAndPresets(): void public function testNodeGetReturnsFullFormShortformAndPresets(): void
{ {
DiviPageLoaderTest::givenPost(DiviPageLoaderTest::fixture('synthetic-basic.html')); DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
$ability = new DiviNodeGetAbility(new DiviPageLoader(DiviPageLoaderTest::schema()), $this->design()); $ability = new DiviNodeGetAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
$heading = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c1.m0']); $heading = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c1.m0']);
@@ -117,8 +117,8 @@ final class DiviReadAbilitiesTest extends TestCase
public function testNodeGetValidatesTheId(): void public function testNodeGetValidatesTheId(): void
{ {
DiviPageLoaderTest::givenPost(DiviPageLoaderTest::fixture('synthetic-basic.html')); DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
$ability = new DiviNodeGetAbility(new DiviPageLoader(DiviPageLoaderTest::schema()), $this->design()); $ability = new DiviNodeGetAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
self::assertSame('lit_validation_failed', $ability->execute(['post_id' => 7, 'id' => 'hero'])->get_error_code()); self::assertSame('lit_validation_failed', $ability->execute(['post_id' => 7, 'id' => 'hero'])->get_error_code());
self::assertSame('lit_not_found', $ability->execute(['post_id' => 7, 'id' => 's5.r0'])->get_error_code()); self::assertSame('lit_not_found', $ability->execute(['post_id' => 7, 'id' => 's5.r0'])->get_error_code());
@@ -7,21 +7,14 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use Brain\Monkey\Functions; use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Rev; use LIT\MCP\Abilities\Rev;
use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap; use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex; use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\Parser; use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\BlockTree\Serializer; use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader; use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Pages\DiviReplaceTextAbility; use LIT\MCP\Integrations\Divi\Pages\DiviReplaceTextAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver; use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter; use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery; use Mockery;
use RuntimeException; use RuntimeException;
@@ -47,29 +40,10 @@ final class DiviReplaceTextAbilityTest extends TestCase
/** @param list<int> $ids Rows of the prefilter query. */ /** @param list<int> $ids Rows of the prefilter query. */
private function ability(?string $content = null, bool $unfiltered = false, array $ids = [7]): DiviReplaceTextAbility private function ability(?string $content = null, bool $unfiltered = false, array $ids = [7]): DiviReplaceTextAbility
{ {
$this->post = DiviPageLoaderTest::givenPost($content ?? DiviPageLoaderTest::fixture('synthetic-basic.html')); $this->post = DiviFixtures::givenPost($content ?? DiviFixtures::markup('synthetic-basic.html'));
Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['edit_posts' => 'edit_pages', 'edit_others_posts' => 'edit_others_pages']]); Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['edit_posts' => 'edit_pages', 'edit_others_posts' => 'edit_others_pages']]);
Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true'); Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true');
$schema = DiviPageLoaderTest::schema(); $schema = DiviFixtures::schema();
$modules = Mockery::mock(DiviModules::class);
$modules->allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog());
$modules->allows('builderVersion')->andReturn('5.2.1');
$design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DesignSystemTest::sample());
$composition = new DiviComposition($modules, $design, ComposeFixture::sanitizer($unfiltered), ComposeFixture::media());
$saver = Mockery::mock(DiviSaver::class);
$saver->allows('save')->andReturnUsing(function (WP_Post $post, string $content): array {
$failure = array_shift($this->failures);
if ($failure !== null) {
throw $failure;
}
$this->saved[] = $content;
$post->post_content = $content;
return ['revision_id' => 811, 'warnings' => []];
});
$log = Mockery::mock(ChangeLog::class);
$log->allows('record')->andReturn(4711);
$loader = new DiviPageLoader($schema); $loader = new DiviPageLoader($schema);
$db = Mockery::mock(\wpdb::class)->makePartial(); $db = Mockery::mock(\wpdb::class)->makePartial();
$db->allows('get_col')->andReturnUsing(function (string $sql) use ($ids): array { $db->allows('get_col')->andReturnUsing(function (string $sql) use ($ids): array {
@@ -81,7 +55,7 @@ final class DiviReplaceTextAbilityTest extends TestCase
return new DiviReplaceTextAbility( return new DiviReplaceTextAbility(
$schema, $schema,
$loader, $loader,
new DiviWriter($loader, $saver, $log, $design, $composition), DiviWriterFixture::writer($this->saved, $this->failures, $unfiltered, $loader),
$db $db
); );
} }
@@ -6,26 +6,19 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use Brain\Monkey\Functions; use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Rev; use LIT\MCP\Abilities\Rev;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap; use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex; use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\Parser; use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility; use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviDuplicateNodeAbility; use LIT\MCP\Integrations\Divi\Pages\DiviDuplicateNodeAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviInsertAbility; use LIT\MCP\Integrations\Divi\Pages\DiviInsertAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviMoveAbility; use LIT\MCP\Integrations\Divi\Pages\DiviMoveAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver; use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Pages\DiviUpdateAbility; use LIT\MCP\Integrations\Divi\Pages\DiviUpdateAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter; use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\Divi\Schema\DiviModules; use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture; use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Error; use WP_Error;
use WP_Post; use WP_Post;
@@ -42,34 +35,10 @@ final class DiviWriteAbilitiesTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->original = DiviPageLoaderTest::fixture('synthetic-basic.html'); $this->original = DiviFixtures::markup('synthetic-basic.html');
$this->post = DiviPageLoaderTest::givenPost($this->original); $this->post = DiviFixtures::givenPost($this->original);
Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true'); Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true');
$this->writer = DiviWriterFixture::writer($this->saved);
$modules = Mockery::mock(DiviModules::class);
$modules->allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog());
$modules->allows('builderVersion')->andReturn('5.2.1');
$design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DesignSystemTest::sample());
$composition = new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media());
$saver = Mockery::mock(DiviSaver::class);
$saver->allows('save')->andReturnUsing(function (WP_Post $post, string $content): array {
$this->saved[] = $content;
$post->post_content = $content;
return ['revision_id' => 811, 'warnings' => []];
});
$log = Mockery::mock(ChangeLog::class);
$log->allows('record')->andReturn(4711);
$this->writer = new DiviWriter(
new DiviPageLoader(DiviPageLoaderTest::schema()),
$saver,
$log,
$design,
$composition
);
} }
private function treeRev(): string private function treeRev(): string
@@ -274,7 +243,7 @@ final class DiviWriteAbilitiesTest extends TestCase
/** StructureRules apply on move: a module directly into a section, or into a specialty column. */ /** StructureRules apply on move: a module directly into a section, or into a specialty column. */
public function testMoveRefusedByStructureRules(): void public function testMoveRefusedByStructureRules(): void
{ {
$mixed = DiviPageLoaderTest::fixture('synthetic-mixed.html'); $mixed = DiviFixtures::markup('synthetic-mixed.html');
$this->post->post_content = $mixed; $this->post->post_content = $mixed;
$treeRev = Rev::of($mixed); $treeRev = Rev::of($mixed);
@@ -342,7 +311,7 @@ final class DiviWriteAbilitiesTest extends TestCase
/** An opaque/global module is removed as a whole – it never gets descended into. */ /** An opaque/global module is removed as a whole – it never gets descended into. */
public function testDeleteRemovesAnOpaqueGlobalModuleAsAWhole(): void public function testDeleteRemovesAnOpaqueGlobalModuleAsAWhole(): void
{ {
$mixed = DiviPageLoaderTest::fixture('synthetic-mixed.html'); $mixed = DiviFixtures::markup('synthetic-mixed.html');
$this->post->post_content = $mixed; $this->post->post_content = $mixed;
$globalRow = '<!-- wp:divi/row {"globalModule":"913","module":{"meta":{"adminLabel":{"desktop":{"value":' $globalRow = '<!-- wp:divi/row {"globalModule":"913","module":{"meta":{"adminLabel":{"desktop":{"value":'
. '"Globale Zeile"}}}}} --><!-- wp:divi/column {"builderVersion":"5.2.1"} --><!-- wp:divi/text ' . '"Globale Zeile"}}}}} --><!-- wp:divi/column {"builderVersion":"5.2.1"} --><!-- wp:divi/text '
@@ -360,7 +329,7 @@ final class DiviWriteAbilitiesTest extends TestCase
public function testDeletingTheLastChildLeavesAValidTree(): void public function testDeletingTheLastChildLeavesAValidTree(): void
{ {
$mixed = DiviPageLoaderTest::fixture('synthetic-mixed.html'); $mixed = DiviFixtures::markup('synthetic-mixed.html');
$this->post->post_content = $mixed; $this->post->post_content = $mixed;
$result = $this->delete()->execute([ $result = $this->delete()->execute([
@@ -443,7 +412,7 @@ final class DiviWriteAbilitiesTest extends TestCase
/** A global module copy stays a byte-identical reference (read-only nodes move/duplicate as a whole). */ /** A global module copy stays a byte-identical reference (read-only nodes move/duplicate as a whole). */
public function testDuplicateGlobalModuleCopyIsAByteIdenticalReference(): void public function testDuplicateGlobalModuleCopyIsAByteIdenticalReference(): void
{ {
$mixed = DiviPageLoaderTest::fixture('synthetic-mixed.html'); $mixed = DiviFixtures::markup('synthetic-mixed.html');
$this->post->post_content = $mixed; $this->post->post_content = $mixed;
$globalRow = '<!-- wp:divi/row {"globalModule":"913","module":{"meta":{"adminLabel":{"desktop":{"value":' $globalRow = '<!-- wp:divi/row {"globalModule":"913","module":{"meta":{"adminLabel":{"desktop":{"value":'
. '"Globale Zeile"}}}}} --><!-- wp:divi/column {"builderVersion":"5.2.1"} --><!-- wp:divi/text ' . '"Globale Zeile"}}}}} --><!-- wp:divi/column {"builderVersion":"5.2.1"} --><!-- wp:divi/text '
@@ -528,7 +497,7 @@ final class DiviWriteAbilitiesTest extends TestCase
public function testReadOnlyNodesCannotBeUpdated(): void public function testReadOnlyNodesCannotBeUpdated(): void
{ {
$this->post->post_content = DiviPageLoaderTest::fixture('synthetic-mixed.html'); $this->post->post_content = DiviFixtures::markup('synthetic-mixed.html');
$result = $this->update()->execute([ $result = $this->update()->execute([
'post_id' => 7, 'post_id' => 7,
@@ -17,10 +17,8 @@ use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver; use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter; use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap; use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules; use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture; use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery; use Mockery;
use WP_Post; use WP_Post;
@@ -32,45 +30,41 @@ final class DiviWriterTest extends TestCase
private function writer(DiviSaver $saver, ChangeLog $log): DiviWriter private function writer(DiviSaver $saver, ChangeLog $log): DiviWriter
{ {
$modules = Mockery::mock(DiviModules::class); $design = DiviWriterFixture::designReader();
$modules->allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog());
$modules->allows('builderVersion')->andReturn('5.2.1');
$design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DesignSystemTest::sample());
$registry = new DynamicContentMap(function (int $postId): array { $registry = new DynamicContentMap(function (int $postId): array {
$this->registryCalls[] = $postId; $this->registryCalls[] = $postId;
return DynamicContentMapTest::registry(); return DiviFixtures::dynamicContentRegistry();
}); });
return new DiviWriter( return new DiviWriter(
new DiviPageLoader(DiviPageLoaderTest::schema()), new DiviPageLoader(DiviFixtures::schema()),
$saver, $saver,
$log, $log,
$design, $design,
new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media(), $registry) DiviWriterFixture::composition($design, false, $registry)
); );
} }
public function testPageChecksTheTreeRevLock(): void public function testPageChecksTheTreeRevLock(): void
{ {
$content = DiviPageLoaderTest::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
DiviPageLoaderTest::givenPost($content); DiviFixtures::givenPost($content);
$writer = $this->writer(Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class)); $writer = $this->writer(Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class));
self::assertInstanceOf(DiviPage::class, $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)])); self::assertInstanceOf(DiviPage::class, $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]));
self::assertSame('lit_rev_mismatch', $writer->page(['post_id' => 7])->get_error_code()); self::assertSame('lit_rev_mismatch', $writer->page(['post_id' => 7])->get_error_code());
$stale = $writer->page(['post_id' => 7, 'tree_rev' => Rev::ofPost(DiviPageLoaderTest::givenPost($content))]); $stale = $writer->page(['post_id' => 7, 'tree_rev' => Rev::ofPost(DiviFixtures::givenPost($content))]);
self::assertSame('lit_rev_mismatch', $stale->get_error_code(), 'the post rev is no tree_rev'); self::assertSame('lit_rev_mismatch', $stale->get_error_code(), 'the post rev is no tree_rev');
self::assertStringContainsString('nichts wurde', $stale->get_error_message()); self::assertStringContainsString('nichts wurde', $stale->get_error_message());
} }
public function testPageRefusesWhatIsNotWritable(): void public function testPageRefusesWhatIsNotWritable(): void
{ {
$content = DiviPageLoaderTest::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
DiviPageLoaderTest::givenPost($content); DiviFixtures::givenPost($content);
$writer = new DiviWriter( $writer = new DiviWriter(
new DiviPageLoader(DiviPageLoaderTest::schema(false)), new DiviPageLoader(DiviFixtures::schema(false)),
Mockery::mock(DiviSaver::class), Mockery::mock(DiviSaver::class),
Mockery::mock(ChangeLog::class), Mockery::mock(ChangeLog::class),
Mockery::mock(DesignSystemReader::class), Mockery::mock(DesignSystemReader::class),
@@ -82,8 +76,8 @@ final class DiviWriterTest extends TestCase
public function testSectionLabelOfANode(): void public function testSectionLabelOfANode(): void
{ {
DiviPageLoaderTest::givenPost(DiviPageLoaderTest::fixture('synthetic-basic.html')); DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
$page = (new DiviPageLoader(DiviPageLoaderTest::schema()))->load(7, true); $page = (new DiviPageLoader(DiviFixtures::schema()))->load(7, true);
self::assertSame('Hero', DiviWriter::sectionLabel($page->index, $page->index->find('s0.r0.c1'))); self::assertSame('Hero', DiviWriter::sectionLabel($page->index, $page->index->find('s0.r0.c1')));
self::assertSame('', DiviWriter::sectionLabel($page->index, null)); self::assertSame('', DiviWriter::sectionLabel($page->index, null));
@@ -91,8 +85,8 @@ final class DiviWriterTest extends TestCase
public function testCompositionIsBoundToThePage(): void public function testCompositionIsBoundToThePage(): void
{ {
$content = DiviPageLoaderTest::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
DiviPageLoaderTest::givenPost($content); DiviFixtures::givenPost($content);
$writer = $this->writer(Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class)); $writer = $this->writer(Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class));
$page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]);
@@ -103,8 +97,8 @@ final class DiviWriterTest extends TestCase
public function testUnchangedTreeIsNotSaved(): void public function testUnchangedTreeIsNotSaved(): void
{ {
$content = DiviPageLoaderTest::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
DiviPageLoaderTest::givenPost($content); DiviFixtures::givenPost($content);
$saver = Mockery::mock(DiviSaver::class); $saver = Mockery::mock(DiviSaver::class);
$saver->expects('save')->never(); $saver->expects('save')->never();
$writer = $this->writer($saver, Mockery::mock(ChangeLog::class)); $writer = $this->writer($saver, Mockery::mock(ChangeLog::class));
@@ -118,13 +112,13 @@ final class DiviWriterTest extends TestCase
public function testSavesAndReportsChangedNodesOutlineAndTheNewTreeRev(): void public function testSavesAndReportsChangedNodesOutlineAndTheNewTreeRev(): void
{ {
$content = DiviPageLoaderTest::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
DiviPageLoaderTest::givenPost($content); DiviFixtures::givenPost($content);
$stored = null; $stored = null;
$saver = Mockery::mock(DiviSaver::class); $saver = Mockery::mock(DiviSaver::class);
$saver->expects('save')->andReturnUsing(static function (WP_Post $post, string $new) use (&$stored): array { $saver->expects('save')->andReturnUsing(static function (WP_Post $post, string $new) use (&$stored): array {
$stored = $new; $stored = $new;
DiviPageLoaderTest::givenPost($new); DiviFixtures::givenPost($new);
return ['revision_id' => 811, 'warnings' => ['vom Speichern']]; return ['revision_id' => 811, 'warnings' => ['vom Speichern']];
}); });
@@ -163,13 +157,13 @@ final class DiviWriterTest extends TestCase
self::assertStringNotContainsString('Leistungen', (string) $result['outline'], 'only affected sections'); self::assertStringNotContainsString('Leistungen', (string) $result['outline'], 'only affected sections');
self::assertContains('eigene Warnung', $result['warnings']); self::assertContains('eigene Warnung', $result['warnings']);
self::assertContains('vom Speichern', $result['warnings']); self::assertContains('vom Speichern', $result['warnings']);
self::assertSame(Rev::ofPost(DiviPageLoaderTest::givenPost($stored)), $result['rev']); self::assertSame(Rev::ofPost(DiviFixtures::givenPost($stored)), $result['rev']);
} }
public function testFailedRollbackIsLoggedAsPartial(): void public function testFailedRollbackIsLoggedAsPartial(): void
{ {
$content = DiviPageLoaderTest::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
DiviPageLoaderTest::givenPost($content); DiviFixtures::givenPost($content);
$saver = Mockery::mock(DiviSaver::class); $saver = Mockery::mock(DiviSaver::class);
$saver->allows('save')->andThrow(WriteFailed::partial(DiviSaver::REASON, postId: 7, revisionId: 811)); $saver->allows('save')->andThrow(WriteFailed::partial(DiviSaver::REASON, postId: 7, revisionId: 811));
$log = Mockery::mock(ChangeLog::class); $log = Mockery::mock(ChangeLog::class);
@@ -184,8 +178,8 @@ final class DiviWriterTest extends TestCase
public function testCleanFailureIsNotLogged(): void public function testCleanFailureIsNotLogged(): void
{ {
$content = DiviPageLoaderTest::fixture('synthetic-basic.html'); $content = DiviFixtures::markup('synthetic-basic.html');
DiviPageLoaderTest::givenPost($content); DiviFixtures::givenPost($content);
$saver = Mockery::mock(DiviSaver::class); $saver = Mockery::mock(DiviSaver::class);
$saver->allows('save')->andThrow(WriteFailed::rolledBack(DiviSaver::REASON, postId: 7, revisionId: 811)); $saver->allows('save')->andThrow(WriteFailed::rolledBack(DiviSaver::REASON, postId: 7, revisionId: 811));
$log = Mockery::mock(ChangeLog::class); $log = Mockery::mock(ChangeLog::class);
@@ -8,71 +8,16 @@ use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Compose\Placeholders; use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\VariableRef; use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap; use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture; use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest; use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use RuntimeException; use RuntimeException;
final class DynamicContentMapTest extends TestCase final class DynamicContentMapTest extends TestCase
{ {
/**
* Shape of DynamicContentOptions::get_options($pageId, 'edit') in Divi 5.2.1 (excerpt of a
* live dump for a page: no post_categories/post_tags – pages have no such taxonomies).
*
* @return array<string, array<string, mixed>>
*/
public static function registry(): array
{
$text = static fn (): array => ['label' => 'x', 'type' => 'text', 'default' => ''];
$option = static fn (string $id, string $group, array $fields): array => [
'id' => $id, 'label' => $id, 'type' => 'text', 'custom' => false, 'group' => $group, 'fields' => $fields,
];
return [
'post_title' => $option('post_title', 'Default', ['before' => $text(), 'after' => $text()]),
'post_excerpt' => $option('post_excerpt', 'Default', [
'before' => $text(), 'after' => $text(), 'words' => $text(), 'read_more_label' => $text(),
]),
'post_date' => $option('post_date', 'Default', [
'before' => $text(),
'after' => $text(),
'date_format' => ['label' => 'x', 'type' => 'select', 'default' => 'default'],
'custom_date_format' => $text(),
]),
'post_author' => $option('post_author', 'Default', [
'before' => $text(),
'after' => $text(),
'name_format' => ['type' => 'select'],
'link' => ['type' => 'yes_no_button'],
]),
'site_title' => $option('site_title', 'Default', ['before' => $text(), 'after' => $text()]),
'post_link_url' => $option('post_link_url', 'Default', []),
'post_link_url_page' => $option('post_link_url_page', 'Default', ['post_id' => ['type' => 'select_post']]),
'post_featured_image' => $option('post_featured_image', 'Default', []),
'post_meta_key' => $option('post_meta_key', 'Standard', [
'before' => $text(),
'after' => $text(),
'select_meta_key' => ['type' => 'select'],
'meta_key' => $text(),
'enable_html' => ['type' => 'yes_no_button'],
]),
'loop_post_title' => $option('loop_post_title', 'Loop', [
'before' => $text(), 'after' => $text(), 'loop_position' => $text(),
]),
'loop_post_meta_key_manual_custom_field' => $option('loop_post_meta_key_manual_custom_field', 'Loop', [
'loop_meta_key' => $text(),
]),
// Odd entries a third-party filter could add.
'broken_fields' => $option('broken_fields', 'Default', ['a' => 'not-an-array', 'b' => ['type' => 3]]),
'no_fields' => ['id' => 'no_fields', 'type' => 'text'],
7 => $option('7', 'Default', []),
'not_an_array' => 'x',
];
}
public function testFlattensTheRegistryToOptionNameAndFieldTypes(): void public function testFlattensTheRegistryToOptionNameAndFieldTypes(): void
{ {
$map = DynamicContentMap::fromOptions(self::registry()); $map = DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry());
// before/after: rich text (Divi outputs them as HTML; text() would trim "Von "). // before/after: rich text (Divi outputs them as HTML; text() would trim "Von ").
self::assertSame(['before' => 'richtext', 'after' => 'richtext'], $map['post_title']); self::assertSame(['before' => 'richtext', 'after' => 'richtext'], $map['post_title']);
@@ -92,7 +37,7 @@ final class DynamicContentMapTest extends TestCase
public function testLoopOptionsNeverEnterTheMap(): void public function testLoopOptionsNeverEnterTheMap(): void
{ {
$map = DynamicContentMap::fromOptions(self::registry()); $map = DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry());
self::assertSame([], array_filter(array_keys($map), static fn (string $name): bool => str_starts_with($name, 'loop_'))); self::assertSame([], array_filter(array_keys($map), static fn (string $name): bool => str_starts_with($name, 'loop_')));
} }
@@ -101,7 +46,7 @@ final class DynamicContentMapTest extends TestCase
{ {
// The map replaces VariableRef's default: every {{post:…}} placeholder must stay usable // The map replaces VariableRef's default: every {{post:…}} placeholder must stay usable
// with the settings it writes (before/after), also where the registry lacks the option. // with the settings it writes (before/after), also where the registry lacks the option.
$map = DynamicContentMap::fromOptions(self::registry()); $map = DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry());
foreach (Placeholders::POST as $option) { foreach (Placeholders::POST as $option) {
self::assertArrayHasKey($option, $map, $option); self::assertArrayHasKey($option, $map, $option);
@@ -115,7 +60,7 @@ final class DynamicContentMapTest extends TestCase
self::assertSame('select', $map['post_date']['date_format']); self::assertSame('select', $map['post_date']['date_format']);
self::assertSame('text', $map['post_excerpt']['words']); self::assertSame('text', $map['post_excerpt']['words']);
$placeholders = new Placeholders(DesignSystemTest::sample(), $map); $placeholders = new Placeholders(DiviFixtures::designSystem(), $map);
foreach (array_keys(Placeholders::POST) as $key) { foreach (array_keys(Placeholders::POST) as $key) {
$reference = $placeholders->resolve('{{post:' . $key . '}}'); $reference = $placeholders->resolve('{{post:' . $key . '}}');
self::assertSame( self::assertSame(
@@ -128,7 +73,7 @@ final class DynamicContentMapTest extends TestCase
public function testTheMapWorksInVariableRef(): void public function testTheMapWorksInVariableRef(): void
{ {
$references = new VariableRef(DesignSystemTest::sample(), DynamicContentMap::fromOptions(self::registry())); $references = new VariableRef(DiviFixtures::designSystem(), DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry()));
$sanitizer = ComposeFixture::sanitizer(); $sanitizer = ComposeFixture::sanitizer();
$ref = static fn (string $name, array $settings): string => VariableRef::serialize('content', $name, $settings); $ref = static fn (string $name, array $settings): string => VariableRef::serialize('content', $name, $settings);
@@ -164,7 +109,7 @@ final class DynamicContentMapTest extends TestCase
$map = (new DynamicContentMap(static function (int $postId) use (&$seen): array { $map = (new DynamicContentMap(static function (int $postId) use (&$seen): array {
$seen[] = $postId; $seen[] = $postId;
return DynamicContentMapTest::registry(); return DiviFixtures::dynamicContentRegistry();
}))->forPost(42); }))->forPost(42);
self::assertSame([42], $seen); self::assertSame([42], $seen);
@@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Schema;
use Brain\Monkey\Functions; use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Schema\DiviCatalogAbility; use LIT\MCP\Integrations\Divi\Schema\DiviCatalogAbility;
use LIT\MCP\Integrations\Divi\Schema\DiviModules; use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery; use Mockery;
@@ -15,7 +16,7 @@ final class DiviCatalogAbilityTest extends TestCase
private function ability(): DiviCatalogAbility private function ability(): DiviCatalogAbility
{ {
$modules = Mockery::mock(DiviModules::class); $modules = Mockery::mock(DiviModules::class);
$modules->allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog()); $modules->allows('catalog')->andReturn(DiviFixtures::catalog());
return new DiviCatalogAbility($modules); return new DiviCatalogAbility($modules);
} }
@@ -7,27 +7,14 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Schema;
use LIT\MCP\Integrations\Divi\Schema\AttrPathIndex; use LIT\MCP\Integrations\Divi\Schema\AttrPathIndex;
use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog; use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
use LIT\MCP\Integrations\Divi\Schema\ShortformMap; use LIT\MCP\Integrations\Divi\Schema\ShortformMap;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
final class ModuleCatalogTest extends TestCase final class ModuleCatalogTest extends TestCase
{ {
/** Real Divi 5.2.1 metadata (tests/Integration/divi-metadata-fixture.php). */
public static function sampleCatalog(): ModuleCatalog
{
$sample = json_decode(
(string) file_get_contents(dirname(__DIR__, 4) . '/Fixtures/divi/metadata-sample.json'),
true
);
return new ModuleCatalog(
$sample['metadata'],
static fn (string $name): array => (array) ($sample['defaults'][$name] ?? [])
);
}
public function testKnowsModulesTitlesCategoriesAndChildren(): void public function testKnowsModulesTitlesCategoriesAndChildren(): void
{ {
$catalog = self::sampleCatalog(); $catalog = DiviFixtures::catalog();
self::assertTrue($catalog->has('divi/button')); self::assertTrue($catalog->has('divi/button'));
self::assertFalse($catalog->has('button')); self::assertFalse($catalog->has('button'));
@@ -44,7 +31,7 @@ final class ModuleCatalogTest extends TestCase
public function testDerivesAttributePathsFromSettingsAndDefaults(): void public function testDerivesAttributePathsFromSettingsAndDefaults(): void
{ {
$catalog = self::sampleCatalog(); $catalog = DiviFixtures::catalog();
foreach ( foreach (
[ [
@@ -93,7 +80,7 @@ final class ModuleCatalogTest extends TestCase
public function testEveryShortformPathIsKnownForTheSampledModules(): void public function testEveryShortformPathIsKnownForTheSampledModules(): void
{ {
$catalog = self::sampleCatalog(); $catalog = DiviFixtures::catalog();
foreach (['text', 'heading', 'button', 'image', 'blurb', 'code', 'accordion-item'] as $type) { foreach (['text', 'heading', 'button', 'image', 'blurb', 'code', 'accordion-item'] as $type) {
foreach (ShortformMap::fields($type) as $field => $spec) { foreach (ShortformMap::fields($type) as $field => $spec) {
self::assertTrue( self::assertTrue(
@@ -131,7 +118,7 @@ final class ModuleCatalogTest extends TestCase
public function testDerivesAttributePathKindsFromComponentMetadataWherePossible(): void public function testDerivesAttributePathKindsFromComponentMetadataWherePossible(): void
{ {
$catalog = self::sampleCatalog(); $catalog = DiviFixtures::catalog();
self::assertSame('richtext', $catalog->pathKind('divi/text', 'content.innerContent')); self::assertSame('richtext', $catalog->pathKind('divi/text', 'content.innerContent'));
self::assertSame('text', $catalog->pathKind('divi/heading', 'title.innerContent')); self::assertSame('text', $catalog->pathKind('divi/heading', 'title.innerContent'));
@@ -154,7 +141,7 @@ final class ModuleCatalogTest extends TestCase
public function testLeavesCompositeAttributePathsUnknownRatherThanGuessing(): void public function testLeavesCompositeAttributePathsUnknownRatherThanGuessing(): void
{ {
$catalog = self::sampleCatalog(); $catalog = DiviFixtures::catalog();
// button.innerContent's only attrName-annotated node is the "link" sub-group itself // button.innerContent's only attrName-annotated node is the "link" sub-group itself
// (component.type "group": link text + linkUrl + linkTarget), and its "text" sub-field // (component.type "group": link text + linkUrl + linkTarget), and its "text" sub-field
@@ -172,7 +159,7 @@ final class ModuleCatalogTest extends TestCase
*/ */
public function testDerivesPerSubNameKindsForCompositePaths(): void public function testDerivesPerSubNameKindsForCompositePaths(): void
{ {
$catalog = self::sampleCatalog(); $catalog = DiviFixtures::catalog();
// imageIcon.innerContent (divi/blurb): useIcon (toggle), icon (icon-picker), src // imageIcon.innerContent (divi/blurb): useIcon (toggle), icon (icon-picker), src
// (upload, image) each carry attrName + subName + their own field component. // (upload, image) each carry attrName + subName + their own field component.
@@ -9,18 +9,13 @@ use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\Rev; use LIT\MCP\Abilities\Rev;
use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer; use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver; use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\WordPress\Content\ContentComposer; use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
use LIT\MCP\Integrations\WordPress\Content\ContentCreateAbility; use LIT\MCP\Integrations\WordPress\Content\ContentCreateAbility;
use LIT\MCP\Integrations\WordPress\Content\PostWriter; use LIT\MCP\Integrations\WordPress\Content\PostWriter;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture; use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
use Mockery; use Mockery;
use Mockery\MockInterface; use Mockery\MockInterface;
@@ -215,11 +210,7 @@ final class ContentCreateDiviTest extends TestCase
*/ */
public function testShortFormAndAttrsRulesApplyThroughTheRealComposer(): void public function testShortFormAndAttrsRulesApplyThroughTheRealComposer(): void
{ {
$modules = Mockery::mock(DiviModules::class); $design = DiviWriterFixture::designReader();
$modules->allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog());
$modules->allows('builderVersion')->andReturn('5.2.1');
$design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DesignSystemTest::sample());
$saved = []; $saved = [];
$saver = Mockery::mock(DiviSaver::class); $saver = Mockery::mock(DiviSaver::class);
$saver->allows('saveNew')->andReturnUsing(static function (int $postId, string $content) use (&$saved): array { $saver->allows('saveNew')->andReturnUsing(static function (int $postId, string $content) use (&$saved): array {
@@ -229,7 +220,7 @@ final class ContentCreateDiviTest extends TestCase
}); });
$real = new DiviContentComposer( $real = new DiviContentComposer(
$this->schema(), $this->schema(),
new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media()), DiviWriterFixture::composition($design),
$saver, $saver,
$design $design
); );