*/ private array $registryCalls = []; private function writer(DiviSaver $saver, ChangeLog $log): DiviWriter { $design = DiviWriterFixture::designReader(); $registry = new DynamicContentMap(function (int $postId): array { $this->registryCalls[] = $postId; return DiviFixtures::dynamicContentRegistry(); }); return new DiviWriter( new DiviPageLoader(DiviFixtures::schema()), $saver, $log, $design, DiviWriterFixture::composition($design, false, $registry) ); } public function testPageChecksTheTreeRevLock(): void { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); $writer = $this->writer(Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class)); self::assertInstanceOf(DiviPage::class, $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)])); self::assertSame('lit_rev_mismatch', $writer->page(['post_id' => 7])->get_error_code()); $stale = $writer->page(['post_id' => 7, 'tree_rev' => Rev::ofPost(DiviFixtures::givenPost($content))]); self::assertSame('lit_rev_mismatch', $stale->get_error_code(), 'the post rev is no tree_rev'); self::assertStringContainsString('nichts wurde', $stale->get_error_message()); } public function testPageRefusesWhatIsNotWritable(): void { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); $writer = new DiviWriter( new DiviPageLoader(DiviFixtures::schema(false)), Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class), Mockery::mock(DesignSystemReader::class), Mockery::mock(DiviComposition::class) ); self::assertSame('lit_unsupported', $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)])->get_error_code()); } public function testSectionLabelOfANode(): void { DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html')); $page = (new DiviPageLoader(DiviFixtures::schema()))->load(7, true); self::assertSame('Hero', DiviWriter::sectionLabel($page->index, $page->index->find('s0.r0.c1'))); self::assertSame('', DiviWriter::sectionLabel($page->index, null)); } public function testCompositionIsBoundToThePage(): void { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); $writer = $this->writer(Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class)); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $writer->composition($page)->placeholders(); self::assertSame([7], $this->registryCalls, 'the live dynamic content map of post 7 (ruling S22)'); } public function testUnchangedTreeIsNotSaved(): void { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); $saver = Mockery::mock(DiviSaver::class); $saver->expects('save')->never(); $writer = $this->writer($saver, Mockery::mock(ChangeLog::class)); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $result = $writer->save('lit/divi-update', $page, new DiviChange('nichts'), []); self::assertSame(Rev::of($content), $result['tree_rev']); self::assertStringStartsWith('Keine Änderung', $result['summary']); } public function testSavesAndReportsChangedNodesOutlineAndTheNewTreeRev(): void { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); $stored = null; $saver = Mockery::mock(DiviSaver::class); $saver->expects('save')->andReturnUsing(static function (WP_Post $post, string $new) use (&$stored): array { $stored = $new; DiviFixtures::givenPost($new); return ['revision_id' => 811, 'warnings' => ['vom Speichern']]; }); $log = Mockery::mock(ChangeLog::class); $log->expects('record')->with('lit/divi-update', 'page', '7', 'Divi "Start": Button geändert', 811) ->andReturn(55); $input = ['post_id' => 7, 'tree_rev' => Rev::of($content)]; Functions\expect('do_action')->once()->with('lit_mcp_after_save', 7, 'lit/divi-update', $input); Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true'); $writer = $this->writer($saver, $log); $page = $writer->page($input); $button = $page->index->find('s0.r0.c1.m2'); $removed = $page->index->find('s1'); $button->setAttrs(AttrMap::set($button->attrs(), 'button.innerContent.desktop.value.text', 'Neu')); $page->document->children()[0]->removeChild($removed); $change = (new DiviChange('Button geändert')) ->changed($button, 'updated') ->removed('s1', $removed) ->warning('eigene Warnung'); $result = $writer->save('lit/divi-update', $page, $change, $input); self::assertIsString($stored); self::assertStringNotContainsString('wp:divi/blurb', $stored); self::assertSame(Rev::of($stored), $result['tree_rev']); self::assertSame(811, $result['revision_id']); self::assertSame(55, $result['log_id']); self::assertSame('https://kunde.test/?page_id=7&et_fb=1', $result['edit_url']); self::assertSame('https://kunde.test/?page_id=7&preview=true', $result['preview_url']); self::assertSame('Button geändert (Status unverändert: draft).', $result['summary']); self::assertSame( [['s0.r0.c1.m2', 'divi/button', 'updated'], ['s1', 'divi/section', 'deleted']], array_map(static fn (array $c): array => [$c['id'], $c['type'], $c['action']], $result['changed']) ); self::assertStringContainsString('Hero', (string) $result['outline']); self::assertStringNotContainsString('Leistungen', (string) $result['outline'], 'only affected sections'); self::assertContains('eigene Warnung', $result['warnings']); self::assertContains('vom Speichern', $result['warnings']); self::assertSame(Rev::ofPost(DiviFixtures::givenPost($stored)), $result['rev']); } public function testFailedRollbackIsLoggedAsPartial(): void { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); $saver = Mockery::mock(DiviSaver::class); $saver->allows('save')->andThrow(WriteFailed::partial(DiviSaver::REASON, postId: 7, revisionId: 811)); $log = Mockery::mock(ChangeLog::class); $log->expects('recordPartial')->with('lit/divi-delete', 'page', '7', 'Divi "Start": Änderung nur teilweise zurückgenommen', 811); $writer = $this->writer($saver, $log); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $page->document->children()[0]->removeChild($page->index->find('s1')); $this->expectException(WriteFailed::class); $writer->save('lit/divi-delete', $page, new DiviChange('s1 gelöscht'), []); } public function testCleanFailureIsNotLogged(): void { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); $saver = Mockery::mock(DiviSaver::class); $saver->allows('save')->andThrow(WriteFailed::rolledBack(DiviSaver::REASON, postId: 7, revisionId: 811)); $log = Mockery::mock(ChangeLog::class); $log->expects('recordPartial')->never(); $log->expects('record')->never(); $writer = $this->writer($saver, $log); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $page->document->children()[0]->removeChild($page->index->find('s1')); $this->expectException(WriteFailed::class); $writer->save('lit/divi-delete', $page, new DiviChange('s1 gelöscht'), []); } }