feat: lit/content-create with Divi short form
New input divi (1-30 sections, short form like lit-divi-insert) builds a Divi 5 layout in one divi/placeholder. The whole description is checked before the insert (Divi's map for "no entry yet"); the draft is inserted with its normal WordPress fields only, then DiviContentComposer composes again bound to the new entry and stores it via DiviSaver::saveNew (same kses window as save(), strict DiviFlags). A refusal or failure deletes the draft. Result adds tree_rev and the outline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\DiviIntegration;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
@@ -44,4 +45,14 @@ final class DiviIntegrationTest extends TestCase
|
||||
self::assertNotNull($ability->outputSchema(), $ability->name());
|
||||
}
|
||||
}
|
||||
|
||||
public function testGivesContentCreateTheDiviShortForm(): void
|
||||
{
|
||||
$integration = new DiviIntegration(Mockery::mock(ContentSchema::class), Mockery::mock(ChangeLog::class));
|
||||
|
||||
$composer = $integration->contentComposer();
|
||||
|
||||
self::assertInstanceOf(DiviContentComposer::class, $composer);
|
||||
self::assertFalse($composer->available(), 'no Divi classes in unit tests');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
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\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||
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 Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
|
||||
final class DiviContentComposerTest extends TestCase
|
||||
{
|
||||
/** @var list<int> Post IDs the dynamic content registry was asked for. */
|
||||
private array $registryCalls = [];
|
||||
|
||||
/** @var array<int, string> Content handed to DiviSaver::saveNew() by post ID. */
|
||||
private array $saved = [];
|
||||
|
||||
/** @var DiviSaver&MockInterface */
|
||||
private DiviSaver $saver;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->saver = Mockery::mock(DiviSaver::class);
|
||||
$this->saver->allows('saveNew')->andReturnUsing(function (int $postId, string $content): array {
|
||||
$this->saved[$postId] = $content;
|
||||
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (callable(int): array<string, mixed>)|null $registry Divi's dynamic content options per post ID.
|
||||
*/
|
||||
private function composer(bool $diviEnabled = true, ?callable $registry = null): DiviContentComposer
|
||||
{
|
||||
$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());
|
||||
$map = new DynamicContentMap(function (int $postId) use ($registry): array {
|
||||
$this->registryCalls[] = $postId;
|
||||
|
||||
return $registry !== null ? $registry($postId) : DynamicContentMapTest::registry();
|
||||
});
|
||||
|
||||
return new DiviContentComposer(
|
||||
DiviPageLoaderTest::schema($diviEnabled),
|
||||
new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media(), $map),
|
||||
$this->saver,
|
||||
$design
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<array<string, mixed>> */
|
||||
private static function sections(): array
|
||||
{
|
||||
return [
|
||||
['type' => 'section', 'label' => 'Hero', 'modules' => [['type' => 'heading', 'text' => 'Willkommen']]],
|
||||
['type' => 'section', 'modules' => [['type' => 'image', 'src' => 'https://kunde.test/a.jpg']]],
|
||||
];
|
||||
}
|
||||
|
||||
public function testChecksBeforeTheEntryExistsWithDivisMapForNoEntry(): void
|
||||
{
|
||||
self::assertNull($this->composer()->check('page', self::sections()));
|
||||
self::assertSame([0], $this->registryCalls, 'Divi\'s options for "no entry yet" (post ID 0)');
|
||||
self::assertSame([], $this->saved, 'check writes nothing');
|
||||
}
|
||||
|
||||
public function testWritesAPlaceholderWrappedLayoutBoundToTheNewEntry(): void
|
||||
{
|
||||
$written = $this->composer()->write(42, 'page', self::sections());
|
||||
|
||||
self::assertIsArray($written);
|
||||
self::assertSame([42], $this->registryCalls, 'the final layout uses the live map of the new entry');
|
||||
$content = $this->saved[42];
|
||||
// Ruling S9: exactly one divi/placeholder around the sections, as TreeEditor does for empty pages.
|
||||
self::assertStringStartsWith('<!-- wp:divi/placeholder --><!-- wp:divi/section ', $content);
|
||||
self::assertStringEndsWith('<!-- /wp:divi/section --><!-- /wp:divi/placeholder -->', $content);
|
||||
self::assertSame(1, substr_count($content, '<!-- wp:divi/placeholder -->'));
|
||||
$document = (new Parser())->parse($content);
|
||||
self::assertCount(1, $document->children());
|
||||
self::assertCount(2, $document->children()[0]->children());
|
||||
self::assertContains('s1.r0.c0.m0: Bild ohne Alt-Text.', $written['warnings']);
|
||||
self::assertStringContainsString('s0 section "Hero"', (string) $written['outline']);
|
||||
}
|
||||
|
||||
public function testRefusesTypesWithoutBuilderAndInvalidNodes(): void
|
||||
{
|
||||
$disabled = $this->composer(false)->check('post', [['type' => 'section']]);
|
||||
self::assertInstanceOf(WP_Error::class, $disabled);
|
||||
self::assertSame('lit_validation_failed', $disabled->get_error_code());
|
||||
self::assertArrayHasKey('divi', $disabled->get_error_data()['fields']);
|
||||
|
||||
$invalid = $this->composer()->check('page', [
|
||||
['type' => 'section', 'modules' => [['type' => 'button', 'attrs' => ['x.y' => ['desktop' => 1]]]]],
|
||||
['type' => 'section', 'modules' => [['type' => 'button', 'url' => 'javascript:alert(1)', 'text' => 'x']]],
|
||||
]);
|
||||
self::assertInstanceOf(WP_Error::class, $invalid);
|
||||
self::assertSame('lit_invalid_attr_path', $invalid->get_error_code());
|
||||
$fields = $invalid->get_error_data()['fields'];
|
||||
self::assertArrayHasKey('divi[0].modules[0].modules[0][0].attrs.x.y', $fields);
|
||||
self::assertGreaterThanOrEqual(2, count($fields), 'every error of every section comes back');
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
/** The entry's own map can be narrower than the one for "no entry": then nothing is written. */
|
||||
public function testWriteRefusesWhatTheNewEntrysMapDoesNotOffer(): void
|
||||
{
|
||||
$registry = static function (int $postId): array {
|
||||
$options = DynamicContentMapTest::registry();
|
||||
if ($postId !== 0) {
|
||||
unset($options['site_title']);
|
||||
}
|
||||
|
||||
return $options;
|
||||
};
|
||||
$title = VariableRef::serialize('content', 'site_title', ['before' => '', 'after' => '']);
|
||||
$sections = [['type' => 'section', 'modules' => [['type' => 'heading', 'text' => $title]]]];
|
||||
|
||||
self::assertNull($this->composer(true, $registry)->check('page', $sections));
|
||||
$written = $this->composer(true, $registry)->write(42, 'page', $sections);
|
||||
|
||||
self::assertInstanceOf(WP_Error::class, $written);
|
||||
self::assertSame('lit_validation_failed', $written->get_error_code());
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testAFailingSaveIsThrown(): void
|
||||
{
|
||||
$this->saver = Mockery::mock(DiviSaver::class);
|
||||
$this->saver->allows('saveNew')->andThrow(new RuntimeException('Divi flag missing'));
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->composer()->write(42, 'page', self::sections());
|
||||
}
|
||||
}
|
||||
@@ -212,4 +212,59 @@ final class DiviSaverTest extends TestCase
|
||||
self::assertStringContainsString('Divi-Kennzeichen', $saved['warnings'][0]);
|
||||
self::assertStringContainsString('divi builder flags failed', (string) file_get_contents($this->log));
|
||||
}
|
||||
|
||||
public function testSaveNewWritesTheFirstContentWithoutKsesAndSetsTheFlagsStrictly(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
Functions\when('has_filter')->justReturn(10);
|
||||
Functions\expect('wp_update_post')->once()->with(['ID' => 42, 'post_content' => 'NEU'], true)
|
||||
->andReturnUsing(function (): int {
|
||||
$this->calls[] = 'update';
|
||||
|
||||
return 42;
|
||||
});
|
||||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 42, 'post_content' => 'NEU']));
|
||||
Functions\expect('wp_save_post_revision')->never();
|
||||
|
||||
self::assertSame([], (new DiviSaver())->saveNew(42, 'NEU'));
|
||||
self::assertSame(
|
||||
// Same kses window as save(), but no restore point: the new draft has no earlier state.
|
||||
[
|
||||
'kses off', 'update', 'kses on',
|
||||
'meta:_et_pb_use_divi_5=on', 'meta:_et_pb_use_builder=on', 'meta:_et_pb_show_page_creation=off',
|
||||
],
|
||||
$this->calls
|
||||
);
|
||||
}
|
||||
|
||||
public function testSaveNewReportsAContentChangedOnSaveAndKeepsKsesForUnfilteredUsers(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('has_filter')->justReturn(10);
|
||||
Functions\when('wp_update_post')->justReturn(42);
|
||||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 42, 'post_content' => 'ANDERS']));
|
||||
|
||||
self::assertSame([DiviSaver::CHANGED_ON_SAVE], (new DiviSaver())->saveNew(42, 'NEU'));
|
||||
self::assertNotContains('kses off', $this->calls);
|
||||
}
|
||||
|
||||
public function testSaveNewThrowsWhenTheUpdateIsRefusedOrAFlagIsMissing(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
Functions\when('has_filter')->justReturn(10);
|
||||
Functions\when('wp_update_post')->justReturn(new WP_Error('db', 'kaputt'));
|
||||
|
||||
try {
|
||||
(new DiviSaver())->saveNew(42, 'NEU');
|
||||
self::fail('RuntimeException expected');
|
||||
} catch (RuntimeException $e) {
|
||||
self::assertStringContainsString('kaputt', $e->getMessage());
|
||||
}
|
||||
self::assertSame(['kses off', 'kses on'], $this->calls, 'kses comes back, no flags on a failed write');
|
||||
|
||||
Functions\when('wp_update_post')->justReturn(42);
|
||||
Functions\when('get_post_meta')->justReturn('');
|
||||
$this->expectException(RuntimeException::class);
|
||||
(new DiviSaver())->saveNew(42, 'NEU');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Content;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
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\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentCreateAbility;
|
||||
use LIT\MCP\Integrations\WordPress\Content\PostWriter;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
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 Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
final class ContentCreateDiviTest extends TestCase
|
||||
{
|
||||
private const MARKUP = '<!-- wp:divi/placeholder --><!-- wp:divi/section --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->';
|
||||
private const SECTIONS = [['type' => 'section']];
|
||||
private const INPUT = ['post_type' => 'page', 'title' => 'Start', 'divi' => self::SECTIONS];
|
||||
|
||||
private function schema(): ContentSchema
|
||||
{
|
||||
Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['create_posts' => 'edit_pages']]);
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('get_current_user_id')->justReturn(3);
|
||||
$schema = Mockery::mock(ContentSchema::class);
|
||||
$schema->allows('isManaged')->andReturn(true);
|
||||
$schema->allows('diviEnabled')->andReturn(true);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/** @return PostWriter&MockInterface */
|
||||
private function writer(): PostWriter
|
||||
{
|
||||
$writer = Mockery::mock(PostWriter::class);
|
||||
$writer->allows('prepare')->andReturn(['fields' => [], 'terms' => [], 'warnings' => []]);
|
||||
$writer->allows('postData')->andReturn(['post_title' => 'Start']);
|
||||
$writer->allows('deniedMeta')->andReturn([]);
|
||||
$writer->allows('writeExtras');
|
||||
$writer->allows('deleteCreatedTerms');
|
||||
|
||||
return $writer;
|
||||
}
|
||||
|
||||
/** @return ContentComposer&MockInterface */
|
||||
private function composer(bool $available = true): ContentComposer
|
||||
{
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->allows('available')->andReturn($available);
|
||||
$composer->allows('check')->with('page', self::SECTIONS)->andReturn(null);
|
||||
|
||||
return $composer;
|
||||
}
|
||||
|
||||
private function log(): ChangeLog
|
||||
{
|
||||
$log = Mockery::mock(ChangeLog::class);
|
||||
$log->allows('record')->andReturn(9);
|
||||
$log->allows('recordPartial');
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
private function ability(?ContentComposer $composer, ?PostWriter $writer = null): ContentCreateAbility
|
||||
{
|
||||
return new ContentCreateAbility($this->schema(), $writer ?? $this->writer(), $this->log(), $composer);
|
||||
}
|
||||
|
||||
private function stubStoredDraft(): void
|
||||
{
|
||||
Functions\when('get_post')->justReturn(new WP_Post([
|
||||
'ID' => 42, 'post_title' => 'Start', 'post_type' => 'page', 'post_status' => 'draft', 'post_content' => self::MARKUP,
|
||||
]));
|
||||
Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=42&preview=true');
|
||||
Functions\when('get_edit_post_link')->justReturn('https://kunde.test/wp-admin/post.php?post=42&action=edit');
|
||||
}
|
||||
|
||||
public function testCreatesADiviDraftWithTreeRevOutlineAndWarnings(): void
|
||||
{
|
||||
$composer = $this->composer();
|
||||
$composer->expects('write')->with(42, 'page', self::SECTIONS)
|
||||
->andReturn(['warnings' => ['s0: leer (section ohne Inhalt).'], 'outline' => 's0 divi/section']);
|
||||
// D7: the insert carries only the normal WordPress fields (kses as usual) – never Divi markup;
|
||||
// the layout is written by the Divi composer through DiviSaver's single kses window.
|
||||
Functions\expect('wp_insert_post')->once()->with([
|
||||
'post_title' => 'Start',
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'draft',
|
||||
'post_author' => 3,
|
||||
], true)->andReturn(42);
|
||||
$this->stubStoredDraft();
|
||||
|
||||
$result = $this->ability($composer)->execute(self::INPUT);
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame(Rev::of(self::MARKUP), $result['tree_rev']);
|
||||
self::assertSame(['s0: leer (section ohne Inhalt).'], $result['warnings']);
|
||||
self::assertSame('s0 divi/section', $result['outline']);
|
||||
self::assertStringContainsString('Entwurf', $result['summary'], 'drafts stay drafts');
|
||||
self::assertArrayHasKey('tree_rev', $this->ability(null)->outputSchema()['properties']);
|
||||
self::assertArrayHasKey('divi', $this->ability(null)->inputSchema()['properties']);
|
||||
}
|
||||
|
||||
public function testWithoutDiviTheComposerIsNotUsed(): void
|
||||
{
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->expects('available')->never();
|
||||
Functions\when('wp_insert_post')->justReturn(42);
|
||||
$this->stubStoredDraft();
|
||||
|
||||
$result = $this->ability($composer)->execute(['post_type' => 'page', 'title' => 'Start']);
|
||||
|
||||
self::assertArrayNotHasKey('tree_rev', $result);
|
||||
}
|
||||
|
||||
public function testDiviNeedsDiviAndExcludesContent(): void
|
||||
{
|
||||
Functions\expect('wp_insert_post')->never();
|
||||
|
||||
$inactive = $this->ability($this->composer(false))->execute(self::INPUT);
|
||||
self::assertSame('lit_unsupported', $inactive->get_error_code());
|
||||
|
||||
$none = $this->ability(null)->execute(self::INPUT);
|
||||
self::assertSame('lit_unsupported', $none->get_error_code());
|
||||
|
||||
$both = $this->ability($this->composer())->execute(self::INPUT + ['content' => '<p>x</p>']);
|
||||
self::assertSame('lit_validation_failed', $both->get_error_code());
|
||||
self::assertArrayHasKey('content', $both->get_error_data()['fields']);
|
||||
}
|
||||
|
||||
public function testEveryValidationErrorComesBackAndNothingIsCreated(): void
|
||||
{
|
||||
Functions\expect('wp_insert_post')->never();
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->allows('available')->andReturn(true);
|
||||
$composer->allows('check')->andReturn(Errors::invalidAttrPath(['divi[0].attrs.x' => 'unbekannt']));
|
||||
$writer = Mockery::mock(PostWriter::class);
|
||||
$writer->allows('prepare')->andReturn(Errors::invalidInput('ungültig', ['fields.role' => 'Pflichtfeld fehlt.']));
|
||||
|
||||
$error = $this->ability($composer, $writer)->execute(self::INPUT);
|
||||
|
||||
self::assertSame('lit_invalid_attr_path', $error->get_error_code());
|
||||
self::assertSame(
|
||||
['fields.role' => 'Pflichtfeld fehlt.', 'divi[0].attrs.x' => 'unbekannt'],
|
||||
$error->get_error_data()['fields']
|
||||
);
|
||||
}
|
||||
|
||||
public function testAFailingLayoutWriteDiscardsTheDraft(): void
|
||||
{
|
||||
$composer = $this->composer();
|
||||
$composer->allows('write')->andThrow(new RuntimeException('Divi flag missing'));
|
||||
Functions\when('wp_insert_post')->justReturn(42);
|
||||
Functions\expect('wp_delete_post')->once()->with(42, true)->andReturn(new WP_Post(['ID' => 42]));
|
||||
|
||||
try {
|
||||
$this->ability($composer)->execute(self::INPUT);
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertTrue($e->rolledBack);
|
||||
}
|
||||
}
|
||||
|
||||
public function testALayoutTheNewEntryRefusesDeletesTheDraftAndReturnsTheErrors(): void
|
||||
{
|
||||
$composer = $this->composer();
|
||||
$composer->allows('write')->andReturn(Errors::invalidInput('ungültig', ['divi[0].modules[0].text' => 'nicht angeboten']));
|
||||
Functions\when('wp_insert_post')->justReturn(42);
|
||||
Functions\expect('wp_delete_post')->once()->with(42, true)->andReturn(new WP_Post(['ID' => 42]));
|
||||
|
||||
$error = $this->ability($composer)->execute(self::INPUT);
|
||||
|
||||
self::assertInstanceOf(WP_Error::class, $error);
|
||||
self::assertSame('lit_validation_failed', $error->get_error_code());
|
||||
self::assertArrayHasKey('divi[0].modules[0].text', $error->get_error_data()['fields']);
|
||||
}
|
||||
|
||||
public function testARefusedLayoutWhoseDraftCannotBeDeletedIsAPartialWrite(): void
|
||||
{
|
||||
$composer = $this->composer();
|
||||
$composer->allows('write')->andReturn(Errors::invalidInput('ungültig', ['divi[0].modules[0].text' => 'nicht angeboten']));
|
||||
Functions\when('wp_insert_post')->justReturn(42);
|
||||
Functions\expect('wp_delete_post')->once()->andReturn(false);
|
||||
|
||||
try {
|
||||
$this->ability($composer)->execute(self::INPUT);
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertFalse($e->rolledBack);
|
||||
self::assertSame(42, $e->postId);
|
||||
self::assertStringContainsString('divi[0].modules[0].text', $e->reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rulings S40/S41 hold for content-create too – they live in the Composer: the same path via
|
||||
* short form and attrs is refused (nothing created); an admin label in attrs wins over label.
|
||||
*/
|
||||
public function testShortFormAndAttrsRulesApplyThroughTheRealComposer(): void
|
||||
{
|
||||
$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());
|
||||
$saved = [];
|
||||
$saver = Mockery::mock(DiviSaver::class);
|
||||
$saver->allows('saveNew')->andReturnUsing(static function (int $postId, string $content) use (&$saved): array {
|
||||
$saved[] = $content;
|
||||
|
||||
return [];
|
||||
});
|
||||
$real = new DiviContentComposer(
|
||||
$this->schema(),
|
||||
new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media()),
|
||||
$saver,
|
||||
$design
|
||||
);
|
||||
self::assertFalse($real->available(), 'no Divi classes in unit tests');
|
||||
// Only available() is replaced (Divi's classes are missing here); check/write are the real ones.
|
||||
$composer = new class ($real) implements ContentComposer {
|
||||
public function __construct(private DiviContentComposer $real)
|
||||
{
|
||||
}
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function check(string $postType, array $sections): ?WP_Error
|
||||
{
|
||||
return $this->real->check($postType, $sections);
|
||||
}
|
||||
|
||||
public function write(int $postId, string $postType, array $sections): array|WP_Error
|
||||
{
|
||||
return $this->real->write($postId, $postType, $sections);
|
||||
}
|
||||
};
|
||||
Functions\when('is_admin')->justReturn(false);
|
||||
$this->stubStoredDraft();
|
||||
|
||||
Functions\expect('wp_insert_post')->never();
|
||||
$overlap = $this->ability($composer)->execute(['post_type' => 'page', 'title' => 'Start', 'divi' => [[
|
||||
'type' => 'section',
|
||||
'label' => 'Kurz',
|
||||
'attrs' => ['module.meta.adminLabel' => ['desktop' => 'Lang']],
|
||||
'modules' => [['type' => 'button', 'text' => 'Los', 'attrs' => ['button.innerContent' => ['desktop' => ['text' => 'x']]]]],
|
||||
]]]);
|
||||
self::assertSame('lit_validation_failed', $overlap->get_error_code());
|
||||
$fields = $overlap->get_error_data()['fields'];
|
||||
self::assertArrayHasKey('divi[0].attrs.module.meta.adminLabel', $fields, 'S41: label and attrs label');
|
||||
self::assertArrayHasKey('divi[0].modules[0].modules[0][0].attrs.button.innerContent', $fields, 'S40');
|
||||
self::assertSame([], $saved);
|
||||
|
||||
Functions\expect('wp_insert_post')->once()->andReturn(42);
|
||||
$label = $this->ability($composer)->execute(['post_type' => 'page', 'title' => 'Start', 'divi' => [[
|
||||
'type' => 'section',
|
||||
'label' => 'Hero',
|
||||
'modules' => [['type' => 'heading', 'text' => 'Hallo', 'attrs' => ['module.meta.adminLabel' => ['desktop' => 'Aus attrs']]]],
|
||||
]]]);
|
||||
self::assertTrue($label['ok']);
|
||||
self::assertStringContainsString('"adminLabel":{"desktop":{"value":"Aus attrs"}}', $saved[0], 'S41: no auto label over it');
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\WordPress;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use LIT\MCP\Integrations\WordPress\WordPressIntegration;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
@@ -61,4 +63,23 @@ final class WordPressIntegrationTest extends TestCase
|
||||
self::assertMatchesRegularExpression('#^lit/[a-z]+(-[a-z]+)+$#', $ability->name());
|
||||
}
|
||||
}
|
||||
|
||||
public function testContentCreateGetsTheContentComposer(): void
|
||||
{
|
||||
Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['create_posts' => 'edit_pages']]);
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$schema = Mockery::mock(ContentSchema::class);
|
||||
$schema->allows('isManaged')->andReturn(true);
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->expects('available')->andReturn(false);
|
||||
$integration = new WordPressIntegration($schema, Mockery::mock(ChangeLog::class), $composer);
|
||||
|
||||
$create = array_values(array_filter(
|
||||
$integration->abilities(),
|
||||
static fn (Ability $ability): bool => $ability->name() === 'lit/content-create'
|
||||
))[0];
|
||||
$result = $create->execute(['post_type' => 'page', 'title' => 'Start', 'divi' => [['type' => 'section']]]);
|
||||
|
||||
self::assertSame('lit_unsupported', $result->get_error_code());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user