Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Library/LibraryMakeGlobalAbilityTest.php
T
janniclas.loosenandClaude Opus 5.5 44c1804c4c feat: lit/library-make-global – node to global module, all or nothing (E10)
- NewLibraryEntry: spec and creation of a new library entry, shared by
  lit/library-save and lit/library-make-global (NewDraft, P7, StoredNodeGuard)
- UsageMatcher::referencesAny() (ruling Q7): a global entry never names any
  entry in any spelling Divi renders (P23/P24 forms, Divi 4 shortcodes inside
  Divi 5 values, values casting to 0); used by library-save scope global and
  make-global
- make-global: entry created, node replaced by GlobalInstance::of, page through
  DiviWriter::commit; refused or rolled-back page deletes the entry, a partial
  page write keeps it (the page may show it); sources are pages and posts only

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-27 21:35:48 +02:00

262 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Rev;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec;
use LIT\MCP\Integrations\Divi\Library\LibraryMakeGlobalAbility;
use LIT\MCP\Integrations\Divi\Library\NewLibraryEntry;
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\SaveScope;
use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Support\LibraryFixtures;
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use Throwable;
use WP_Post;
final class LibraryMakeGlobalAbilityTest extends TestCase
{
private WP_Post $page;
/** @var list<LibraryItemSpec> */
private array $specs = [];
/** @var list<array{int, string, SaveScope}> */
private array $entryContents = [];
/** @var list<string> Page contents DiviWriter saved. */
private array $saved = [];
private FakeChangeLog $log;
protected function setUp(): void
{
parent::setUp();
$this->page = LibraryFixtures::page(DiviFixtures::markup('stage3/page--globale-instanzen.html'));
LibraryFixtures::given([$this->page]);
Functions\when('wp_revisions_enabled')->justReturn(true);
$this->log = DiviWriterFixture::log();
}
/**
* @param list<string> $deniedRoles
* @param list<Throwable> $pageFailures
*/
private function ability(
array $deniedRoles = [],
bool $diviWritable = true,
bool $unfiltered = false,
array $pageFailures = [],
?DiviPageLoader $loader = null,
?DiviWriter $writer = null
): LibraryMakeGlobalAbility {
$store = LibraryFixtures::store();
$store->allows('create')->andReturnUsing(function (LibraryItemSpec $spec): int {
$this->specs[] = $spec;
return 2300;
});
$saver = Mockery::mock(DiviSaver::class);
$saver->allows('saveNew')->andReturnUsing(function (int $id, string $content, SaveScope $scope): array {
$this->entryContents[] = [$id, $content, $scope];
return [];
});
$access = LibraryFixtures::access($deniedRoles, true, $store);
return new LibraryMakeGlobalAbility(
$access,
$writer ?? DiviWriterFixture::writer(
$this->saved,
$pageFailures,
$unfiltered,
$loader ?? LibraryFixtures::loader($access)
),
new NewLibraryEntry($access, $saver, DiviWriterFixture::modules()),
$this->log,
static fn (): bool => $diviWritable
);
}
/** @return array<string, mixed> */
private function input(array $extra = []): array
{
return array_merge(
['post_id' => 7, 'tree_rev' => Rev::of((string) $this->page->post_content), 'id' => 's1', 'title' => 'Leistungen'],
$extra
);
}
public function testReplacesTheNodeByAnInstanceOfTheNewGlobalEntry(): void
{
$result = $this->ability()->execute($this->input());
self::assertTrue($result['ok']);
self::assertSame(2300, $result['library_id']);
self::assertTrue($this->specs[0]->global);
self::assertSame(
str_replace("\r\n", "\n", DiviFixtures::markup('stage3/library--library-abschnitt.html')),
$this->entryContents[0][1],
'the entry holds exactly the node, framed like the builder'
);
self::assertCount(1, $this->saved);
self::assertStringContainsString(
'<!-- wp:divi/global-layout {"globalModule":"2300","blockName":"divi/section","localAttrs":',
$this->saved[0]
);
self::assertStringNotContainsString(
'<!-- wp:divi/section {"module":{"meta":{"adminLabel":{"desktop":{"value":"Library Abschnitt"',
$this->saved[0],
'the plain section is gone'
);
self::assertNotSame([], array_filter($result['warnings'], static fn (string $w): bool => str_contains($w, 'lit/library-find-similar')));
}
public function testEditorCannotMakeACodeModuleGlobal(): void
{
$this->page = LibraryFixtures::page(
'<!-- wp:divi/placeholder --><!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
. '<!-- wp:divi/code {"content":{"innerContent":{"desktop":{"value":"<b>x</b>"}}}} /-->'
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->'
);
LibraryFixtures::given([$this->page]);
$result = $this->ability()->execute($this->input(['id' => 's0']));
self::assertToolError($result, 'validation_failed');
self::assertStringContainsString('Code-Modul', implode(' ', self::errorFields($result)));
self::assertSame([], $this->specs);
self::assertSame([], $this->saved);
}
/** All or nothing: the page save failing removes the new entry again. */
public function testFailedPageSaveDeletesTheNewEntry(): void
{
Functions\expect('wp_delete_post')->once()->with(2300, true)->andReturn(new WP_Post(['ID' => 2300]));
try {
$this->ability([], true, false, [WriteFailed::rolledBack('Die Divi-Seite konnte nicht gespeichert werden.')])
->execute($this->input());
self::fail('no WriteFailed');
} catch (WriteFailed $e) {
self::assertTrue($e->rolledBack);
}
self::assertCount(1, $this->specs);
}
public function testRefusesNodesThatAreOrHoldGlobalModules(): void
{
self::assertToolError($this->ability()->execute($this->input(['id' => 'g0'])), 'validation_failed');
self::assertToolError($this->ability()->execute($this->input(['id' => 's0'])), 'validation_failed');
self::assertSame([], $this->specs);
}
public function testNeedsSaveGlobalAndInsertRightsAndTheDiviGate(): void
{
self::assertToolError($this->ability(['edit_global_library'])->execute($this->input()), 'forbidden');
self::assertToolError($this->ability(['add_library'])->execute($this->input()), 'forbidden');
self::assertToolError($this->ability([], false)->execute($this->input()), 'unsupported');
self::assertToolError($this->ability()->execute($this->input(['tree_rev' => 'alt'])), 'rev_mismatch');
self::assertSame([], $this->specs);
self::assertFalse($this->ability()->isDirect());
}
public function testLogsTheNewEntry(): void
{
$this->ability()->execute($this->input());
self::assertSame('et_pb_layout', $this->log->records[0]['object_type']);
self::assertSame('2300', $this->log->records[0]['object_id']);
self::assertStringContainsString('global', $this->log->records[0]['summary']);
}
/**
* Ruling Q7: a Divi 4 shortcode in a text value renders another entry (or, casting to 0, the newest
* layout – the entry being created); such a node never becomes a global entry.
*/
public function testRefusesAShortcodeReferenceInsideAValue(): void
{
$quote = chr(92) . '"';
foreach (['1058', 'abc'] as $ref) {
$this->page = LibraryFixtures::page(
'<!-- wp:divi/placeholder --><!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
. '<!-- wp:divi/text {"content":{"innerContent":{"desktop":{"value":"[et_pb_text global_module='
. $quote . $ref . $quote . ']x[/et_pb_text]"}}}} /-->'
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->'
);
LibraryFixtures::given([$this->page]);
$result = $this->ability([], true, true)->execute($this->input(['id' => 's0']));
self::assertToolError($result, 'validation_failed');
self::assertStringContainsString('global_module', implode(' ', self::errorFields($result)));
}
self::assertSame([], $this->specs);
self::assertSame([], $this->saved);
}
public function testLibraryEntriesAndThemeBuilderLayoutsAreNoSource(): void
{
$item = LibraryFixtures::item(1055);
LibraryFixtures::given([$item]);
$fromLibrary = $this->ability()->execute(
['post_id' => 1055, 'tree_rev' => Rev::of((string) $item->post_content), 'id' => 's0', 'title' => 'x']
);
self::assertToolError($fromLibrary, 'validation_failed');
self::assertArrayHasKey('post_id', self::errorFields($fromLibrary));
$body = ThemeBuilderFixtures::layout(ThemeBuilderFixtures::BODY);
LibraryFixtures::given([$body]);
Functions\when('admin_url')->alias(static fn (string $path): string => 'https://kunde.test/wp-admin/' . $path);
$loader = new DiviPageLoader(
DiviFixtures::schema(),
new Parser(),
LibraryFixtures::access(),
ThemeBuilderFixtures::access([], true, ThemeBuilderFixtures::store([ThemeBuilderFixtures::template()]))
);
$fromLayout = $this->ability([], true, false, [], $loader)->execute(
['post_id' => 1062, 'tree_rev' => Rev::of((string) $body->post_content), 'id' => 's0', 'title' => 'x']
);
self::assertToolError($fromLayout, 'validation_failed');
self::assertStringContainsString('Theme-Builder', $fromLayout->get_error_message());
self::assertSame([], $this->specs);
self::assertSame([], $this->saved);
}
/** A page write that may have been saved keeps the entry – the page may already show it. */
public function testAPartialPageWriteKeepsTheEntry(): void
{
Functions\expect('wp_delete_post')->never();
$failures = [WriteFailed::partial('Die Divi-Seite konnte nicht gespeichert werden.', postId: 7)];
$design = DiviWriterFixture::designReader();
$writer = new DiviWriter(
LibraryFixtures::loader(),
DiviWriterFixture::saver($this->saved, $failures),
FakeChangeLog::any(),
$design,
DiviWriterFixture::composition($design)
);
try {
$this->ability([], true, false, [], null, $writer)->execute($this->input());
self::fail('no WriteFailed');
} catch (WriteFailed $e) {
self::assertFalse($e->rolledBack);
self::assertSame(2300, $e->details['library_id']);
}
}
}