feat: lit/divi-dynamic-sources mit post_id + node – Schleife des Knotens und passende loop_*-Quellen (X6)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 21:05:49 +02:00
co-authored by Claude Sonnet 5.5
parent 5b52e18a31
commit 02e7c3e8ec
4 changed files with 286 additions and 13 deletions
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Pages\DiviDynamicSourcesAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
@@ -16,6 +17,9 @@ use Mockery;
final class DiviDynamicSourcesAbilityTest extends TestCase
{
/** @var list<int> Post IDs Divi's registry was asked for. */
private array $registryCalls = [];
protected function setUp(): void
{
parent::setUp();
@@ -51,19 +55,118 @@ final class DiviDynamicSourcesAbilityTest extends TestCase
private function ability(
array $deniedRoles = [],
?string $storage = 'meta',
?array $registry = null
?array $registry = null,
?DiviPageLoader $loader = null
): DiviDynamicSourcesAbility {
$registry ??= DiviFixtures::dynamicContentRegistry();
return new DiviDynamicSourcesAbility(
$this->schema(),
new DynamicContentMap(static fn (int $id): array => $registry),
new DynamicContentMap(function (int $id) use ($registry): array {
$this->registryCalls[] = $id;
return $registry;
}),
LibraryFixtures::roles($deniedRoles),
static fn (string $postType): int => 831,
static fn (string $postType): ?string => $storage
static fn (string $postType): ?string => $storage,
$loader
);
}
/**
* Ruling X6: the ability with a loader over post 7 – s0.r0.c0 runs a term loop, s0.r0.c1 none – and a
* registry with one post and one term loop option (Divi 5.15.1 groups and fields).
*/
private function withLoopPage(): DiviDynamicSourcesAbility
{
$loop = ['module' => ['advanced' => ['loop' => ['desktop' => ['value' => [
'enable' => 'on', 'queryType' => 'post_taxonomies',
]]]]]];
$text = '<!-- wp:divi/text {"builderVersion":"5.2.1"} --><!-- /wp:divi/text -->';
$column = static fn (array $attrs): string => '<!-- wp:divi/column '
. json_encode($attrs + ['builderVersion' => '5.2.1']) . ' -->' . $text . '<!-- /wp:divi/column -->';
DiviFixtures::givenPost(
'<!-- wp:divi/placeholder --><!-- wp:divi/section {"builderVersion":"5.2.1"} --><!-- wp:divi/row '
. '{"builderVersion":"5.2.1"} -->' . $column($loop) . $column([])
. '<!-- /wp:divi/row --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->'
);
$field = static fn (string $type): array => ['label' => 'x', 'type' => $type, 'default' => ''];
$common = ['before' => $field('text'), 'after' => $field('text'), 'loop_position' => $field('text')];
$registry = DiviFixtures::dynamicContentRegistry() + [
'loop_post_date' => ['label' => 'Datum', 'group' => 'Loop', 'fields' => $common + [
'date_format' => $field('select'),
]],
'loop_term_name' => ['label' => 'Name', 'group' => 'Loop Terms', 'fields' => $common],
];
return $this->ability([], 'meta', $registry, new DiviPageLoader(DiviFixtures::schema()));
}
/** Ruling X6: post_id + node add the loop sources of the node's loop kind, read from that entry. */
public function testANodeInALoopAddsTheLoopSourcesOfItsKind(): void
{
$result = $this->withLoopPage()->execute(['post_type' => 'page', 'post_id' => 7, 'node' => 's0.r0.c0.m0']);
self::assertSame(7, $result['sample_post_id'], 'the entry itself, not a sample');
self::assertSame([7], array_values(array_unique($this->registryCalls)));
self::assertSame(
['node' => 's0.r0.c0.m0', 'query_type' => 'post_taxonomies', 'label' => 'Terme'],
$result['loop']
);
// The fixture registry also has the meta key option of each kind: only the term one fits, no short form.
self::assertSame(
['loop_term_name', 'loop_term_meta_key_manual_custom_field'],
array_column($result['loop_sources'], 'name')
);
self::assertSame('{{loop:term_name}}', $result['loop_sources'][0]['placeholder']);
self::assertNull($result['loop_sources'][1]['placeholder']);
self::assertEquals(
['before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text'],
(array) $result['loop_sources'][0]['settings']
);
self::assertSame([], array_filter(
array_column($result['sources'], 'name'),
static fn (string $name): bool => str_starts_with($name, 'loop_')
));
}
public function testANodeOutsideALoopGetsNoLoopSources(): void
{
$result = $this->withLoopPage()->execute(['post_type' => 'page', 'post_id' => 7, 'node' => 's0.r0.c1.m0']);
self::assertSame(['node' => 's0.r0.c1.m0', 'query_type' => null, 'label' => null], $result['loop']);
self::assertSame([], $result['loop_sources']);
self::assertStringContainsString('kein Schleifen-Kontext', implode(' ', $result['notes']));
}
public function testWithoutANodeTheAnswerHasNoLoopPart(): void
{
$result = $this->ability()->execute(['post_type' => 'page']);
self::assertArrayNotHasKey('loop', $result);
self::assertArrayNotHasKey('loop_sources', $result);
}
public function testPostIdAndNodeComeTogetherAndNeedTheEntry(): void
{
$ability = $this->withLoopPage();
foreach ([['post_id' => 7], ['node' => 's0']] as $half) {
self::assertToolError($ability->permission(['post_type' => 'page'] + $half), 'validation_failed');
self::assertToolError($ability->execute(['post_type' => 'page'] + $half), 'validation_failed');
}
$node = ['post_type' => 'page', 'post_id' => 7, 'node' => 's0'];
self::assertToolError($ability->execute(['node' => 's9'] + $node), 'not_found');
self::assertToolError($ability->execute(['node' => 'x'] + $node), 'validation_failed');
self::assertToolError($ability->execute(['post_type' => 'post'] + $node), 'validation_failed');
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'edit_post');
self::assertToolError($ability->permission($node), 'forbidden');
self::assertToolError($ability->execute($node), 'forbidden');
self::assertSame([], $this->registryCalls, 'nothing is read for a refused entry');
}
public function testListsDiviSourcesWithTheirPlaceholders(): void
{
$result = $this->ability()->execute(['post_type' => 'page']);