- tests/Support/FakeChangeLog extends ChangeLog: records record()/recordPartial() calls, increasing ids, optional failure; new FakeChangeLog(id) / partialsOnly() / any(id) / forbidden() mirror what the replaced mocks allowed. assertRecorded() / assertPartials() check count and the named arguments. Calls a fake does not allow throw and are also verified in TestCase::tearDown (one assertion per test that uses a fake), so swallowed change-log calls still fail like a never() expectation. - All ChangeLog mocks of ability/integration tests replaced (the admin tests that stub query/purge/clear keep their mocks); expectException tests that checked the partial log now catch and assert. - SchemaFixture::managed(?divi) replaces the 21 isManaged-only ContentSchema mocks; DiviFixtures::schema() uses it. Tests 1397 -> 1404 (FakeChangeLogTest); no test has fewer assertions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
216 lines
9.2 KiB
PHP
216 lines
9.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
|
|
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Abilities\Rev;
|
|
use LIT\MCP\Abilities\WriteFailed;
|
|
use LIT\MCP\ChangeLog\ChangeLog;
|
|
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviChange;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
|
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
|
use LIT\MCP\Tests\Support\DiviFixtures;
|
|
use LIT\MCP\Tests\Support\DiviWriterFixture;
|
|
use LIT\MCP\Tests\Support\FakeChangeLog;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use Mockery;
|
|
use WP_Post;
|
|
|
|
final class DiviWriterTest extends TestCase
|
|
{
|
|
/** @var list<int> */
|
|
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::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),
|
|
FakeChangeLog::forbidden(),
|
|
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), FakeChangeLog::forbidden());
|
|
$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, 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();
|
|
}
|
|
}
|