Contents handed to DiviSaver::save(). */ private array $saved = []; private DiviWriter $writer; protected function setUp(): void { parent::setUp(); $this->original = DiviPageLoaderTest::fixture('synthetic-basic.html'); $this->post = DiviPageLoaderTest::givenPost($this->original); Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true'); $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()); $composition = new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media()); $saver = Mockery::mock(DiviSaver::class); $saver->allows('save')->andReturnUsing(function (WP_Post $post, string $content): array { $this->saved[] = $content; $post->post_content = $content; return ['revision_id' => 811, 'warnings' => []]; }); $log = Mockery::mock(ChangeLog::class); $log->allows('record')->andReturn(4711); $this->writer = new DiviWriter( new DiviPageLoader(DiviPageLoaderTest::schema()), $saver, $log, $design, $composition ); } private function treeRev(): string { return Rev::of($this->original); } private function insert(): DiviInsertAbility { return new DiviInsertAbility($this->writer); } private function update(): DiviUpdateAbility { return new DiviUpdateAbility($this->writer); } /** @return array The structured error the client sees (message JSON, spec §8.2). */ private static function clientError(WP_Error $error): array { return (array) json_decode($error->get_error_message(), true); } public function testInsertComposesAndSavesWithTreeRevAndPostRev(): void { $result = $this->insert()->execute([ 'post_id' => 7, 'tree_rev' => $this->treeRev(), 'after' => 's0.r0.c1.m1', 'nodes' => [['type' => 'text', 'content' => '

Neu

']], ]); self::assertTrue($result['ok'], (string) json_encode($result)); self::assertCount(1, $this->saved); self::assertSame([['id' => 's0.r0.c1.m2', 'type' => 'divi/text', 'action' => 'inserted', 'label' => 'Hero – Text']], $result['changed']); self::assertSame(Rev::of($this->saved[0]), $result['tree_rev']); self::assertSame(Rev::ofPost($this->post), $result['rev']); self::assertSame(811, $result['revision_id']); self::assertSame(4711, $result['log_id']); self::assertStringStartsWith('s0 section "Hero"', (string) $result['outline']); self::assertSame('https://kunde.test/?page_id=7&et_fb=1', $result['edit_url']); // Byte identity: only the new block was added, every other byte is unchanged. $inserted = ''; self::assertSame(str_replace('' . $inserted . '', $this->saved[0]); self::assertSame('s0', $result['changed'][0]['id']); self::assertSame('Start', $result['changed'][0]['label']); } public function testUpdateChangesSeveralNodesAllOrNothing(): void { $failed = $this->update()->execute(['post_id' => 7, 'tree_rev' => $this->treeRev(), 'changes' => [ ['id' => 's0.r0.c1.m2', 'set' => ['text' => 'Jetzt buchen']], ['id' => 's0.r0.c1.m0', 'set' => ['level' => 'h9']], ['id' => 's0.r0.c1.m9', 'label' => 'x'], ]]); self::assertSame('lit_validation_failed', $failed->get_error_code()); $fields = self::clientError($failed)['fields']; self::assertArrayHasKey('changes[1].set.level', $fields); self::assertArrayHasKey('changes[2].id', $fields, 'every error of the call in one response (S39)'); self::assertSame([], $this->saved); $result = $this->update()->execute(['post_id' => 7, 'tree_rev' => $this->treeRev(), 'changes' => [ ['id' => 's0.r0.c1.m2', 'set' => ['text' => 'Jetzt buchen']], ['id' => 's0.r0.c1.m0', 'attrs' => ['title.decoration.font.font' => ['phone' => null]]], ]]); self::assertTrue($result['ok']); self::assertSame(['s0.r0.c1.m2', 's0.r0.c1.m0'], array_column($result['changed'], 'id')); self::assertSame(['updated', 'updated'], array_column($result['changed'], 'action')); self::assertSame(Rev::of($this->saved[0]), $result['tree_rev']); $index = NodeIndex::build((new Parser())->parse($this->saved[0])); self::assertSame('Jetzt buchen', $index->find('s0.r0.c1.m2')?->attrs()['button']['innerContent']['desktop']['value']['text']); self::assertArrayNotHasKey('phone', $index->find('s0.r0.c1.m0')?->attrs()['title']['decoration']['font']['font']); // Untouched section s1 keeps its exact bytes. self::assertStringContainsString(substr($this->original, (int) strpos($this->original, '