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 { $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> */ 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::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()); } }