*/ 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), FakeChangeLog::forbidden()); self::assertInstanceOf(DiviPage::class, $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)])); self::assertToolError($writer->page(['post_id' => 7]), 'rev_mismatch'); $stale = $writer->page(['post_id' => 7, 'tree_rev' => Rev::ofPost(DiviFixtures::givenPost($content))]); self::assertToolError($stale, 'rev_mismatch', '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), FakeChangeLog::forbidden(), Mockery::mock(DesignSystemReader::class), Mockery::mock(DiviComposition::class) ); self::assertToolError($writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]), 'unsupported'); } 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), FakeChangeLog::forbidden()); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $writer->composition($page)->references(); 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, FakeChangeLog::forbidden()); $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 = new FakeChangeLog(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']); $log->assertRecorded([ 'ability' => 'lit/divi-update', 'object_type' => 'page', 'object_id' => '7', 'summary' => 'Divi "Start": Button geändert', 'revision_id' => 811, ]); } 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 = FakeChangeLog::partialsOnly(); $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')); try { $writer->save('lit/divi-delete', $page, new DiviChange('s1 gelöscht'), []); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertFalse($e->rolledBack); } $log->assertPartials([ 'ability' => 'lit/divi-delete', 'object_type' => 'page', 'object_id' => '7', 'summary' => 'Divi "Start": Änderung nur teilweise zurückgenommen', 'revision_id' => 811, ]); } 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 = FakeChangeLog::forbidden(); $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')); try { $writer->save('lit/divi-delete', $page, new DiviChange('s1 gelöscht'), []); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertTrue($e->rolledBack); } $log->assertRecorded(); $log->assertPartials(); } }