Post IDs the dynamic content registry was asked for. */ private array $registryCalls = []; /** @var array 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)|null $registry Divi's dynamic content options per post ID. */ private function composer(bool $diviEnabled = true, ?callable $registry = null): DiviContentComposer { $design = DiviWriterFixture::designReader(); $map = new DynamicContentMap(function (int $postId) use ($registry): array { $this->registryCalls[] = $postId; return $registry !== null ? $registry($postId) : DiviFixtures::dynamicContentRegistry(); }); return new DiviContentComposer( DiviFixtures::schema($diviEnabled), DiviWriterFixture::composition($design, false, $map), $this->saver, $design ); } /** @return list> */ 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('', $content); self::assertSame(1, substr_count($content, '')); $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::assertToolError($disabled, 'validation_failed'); self::assertArrayHasKey('divi', self::errorFields($disabled)); $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::assertToolError($invalid, 'invalid_attr_path'); $fields = self::errorFields($invalid); 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 = DiviFixtures::dynamicContentRegistry(); 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::assertToolError($written, 'validation_failed'); 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()); } }