feat: lit/divi-design-detach entkoppelt Variablen – Verweise in Seiten und Presets werden durch den Wert ersetzt (V9)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6e9218a2bc
commit
e951cc1353
@@ -8,14 +8,18 @@ use Brain\Monkey\Functions;
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignChange;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\DetachRewrite;
|
||||
use LIT\MCP\Integrations\Divi\Design\DiviDesignDetachAbility;
|
||||
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Design\PageRewriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetBook;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery;
|
||||
use LIT\MCP\Integrations\Divi\Design\ReferenceUsageQuery;
|
||||
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
|
||||
use LIT\MCP\Integrations\Divi\Design\VariableDetachRewrite;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Library\UsageReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
@@ -33,6 +37,10 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
||||
{
|
||||
private const LESETEXT = ['group' => 'font-body', 'preset' => 'Lesetext'];
|
||||
|
||||
private const RADIUS = 'gvid-radius0001';
|
||||
|
||||
private const CLAIM = 'gvid-claim00001';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -48,13 +56,15 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
||||
?SnapshotStore $snapshots = null,
|
||||
?FakeChangeLog $log = null,
|
||||
?UsageReport $report = null,
|
||||
bool $unfiltered = true
|
||||
bool $unfiltered = true,
|
||||
?GlobalVariablesAdapter $variables = null
|
||||
): DiviDesignDetachAbility {
|
||||
$usages = Mockery::mock(GlobalUsages::class);
|
||||
$usages->allows('reportFor')->andReturn($report ?? new UsageReport(0, [], 0, 0, [], 0, false));
|
||||
|
||||
return new DiviDesignDetachAbility(
|
||||
$writer ?? self::readOnlyWriter(),
|
||||
$variables ?? self::variableAdapter(),
|
||||
new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), $log ?? FakeChangeLog::forbidden()),
|
||||
$usages,
|
||||
$pages,
|
||||
@@ -77,6 +87,64 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
||||
return $writer;
|
||||
}
|
||||
|
||||
/** Reads variables() (the Variablen-Verwaltung is allowed unless $manage is false), never saves. */
|
||||
private static function variableAdapter(bool $manage = true): GlobalVariablesAdapter&MockInterface
|
||||
{
|
||||
$variables = Mockery::mock(GlobalVariablesAdapter::class);
|
||||
$variables->allows('read')->andReturn(self::variables());
|
||||
$variables->allows('canManage')->andReturn($manage);
|
||||
$variables->expects('save')->never();
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, array<string, mixed>>> Global variables in Divi's stored form. */
|
||||
private static function variables(): array
|
||||
{
|
||||
return [
|
||||
'numbers' => [self::RADIUS => [
|
||||
'id' => self::RADIUS, 'label' => 'Eckenradius', 'value' => '8px', 'order' => '1',
|
||||
'status' => 'active', 'lastUpdated' => '2026-10-01T10:00:00.000Z', 'variableType' => 'numbers',
|
||||
]],
|
||||
// Archived or not does not matter for a detach (V9).
|
||||
'strings' => [self::CLAIM => [
|
||||
'id' => self::CLAIM, 'label' => 'Claim', 'value' => 'Gut beraten', 'order' => '',
|
||||
'status' => 'archived', 'lastUpdated' => '2026-10-01T10:00:00.000Z', 'variableType' => 'strings',
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> PresetBookTest::stored() with the radius variable in preset "CTA primär". */
|
||||
private static function storedWithVariable(): array
|
||||
{
|
||||
$stored = PresetBookTest::stored();
|
||||
$radius = ['topLeft' => VariableDetachRewriteTest::ref(self::RADIUS)];
|
||||
$stored['module']['divi/button']['items']['aaaaaaaaaa']['attrs'] = ['button' => ['decoration' => [
|
||||
'border' => ['desktop' => ['value' => ['radius' => $radius]]],
|
||||
]]];
|
||||
|
||||
return $stored;
|
||||
}
|
||||
|
||||
/** Matches the reference query of a preview()/apply() call: it finds $id and nothing else. */
|
||||
private static function referencing(string $id): Closure
|
||||
{
|
||||
return static fn (ReferenceUsageQuery $query): bool => $query->matches(
|
||||
'<!-- wp:divi/text {"x":"' . $id . '"} /-->'
|
||||
) && ! $query->matches('<!-- wp:divi/text {"x":"gvid-andere0001"} /-->');
|
||||
}
|
||||
|
||||
/** Matches a VariableDetachRewrite strategy that turns a whole reference to $id into $expected. */
|
||||
private static function resolving(string $id, string $expected, array $settings = []): Closure
|
||||
{
|
||||
return static function (Closure $rewrite) use ($id, $expected, $settings): bool {
|
||||
$strategy = $rewrite();
|
||||
$block = Block::create('divi/text', ['x' => VariableDetachRewriteTest::ref($id, $settings)]);
|
||||
|
||||
return $strategy instanceof VariableDetachRewrite && $strategy->rewrite($block) === ['x' => $expected];
|
||||
};
|
||||
}
|
||||
|
||||
private static function untouched(): PageRewriter&MockInterface
|
||||
{
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
@@ -110,14 +178,158 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
||||
self::assertToolError($ability->execute(['preset' => self::LESETEXT]), 'forbidden');
|
||||
}
|
||||
|
||||
public function testVariablesFollowWithStage4c(): void
|
||||
public function testAVariableNeedsTheVariableRightsToo(): void
|
||||
{
|
||||
$error = self::assertToolError(
|
||||
$this->ability(self::untouched())->execute(['variable' => 'Eckenradius']),
|
||||
'unsupported'
|
||||
);
|
||||
$ability = $this->ability(self::untouched(), null, null, null, null, true, self::variableAdapter(false));
|
||||
|
||||
self::assertStringContainsString('Variablen folgen mit Etappe 4c.', $error['hint']);
|
||||
self::assertToolError($ability->permission(['variable' => 'Eckenradius']), 'forbidden');
|
||||
self::assertToolError($ability->execute(['variable' => 'Eckenradius']), 'forbidden');
|
||||
self::assertTrue($ability->permission(['preset' => self::LESETEXT]));
|
||||
}
|
||||
|
||||
public function testUnknownAmbiguousAndColorVariables(): void
|
||||
{
|
||||
$ability = $this->ability(self::untouched());
|
||||
|
||||
$unknown = self::assertToolError($ability->execute(['variable' => 'Fehlt']), 'not_found');
|
||||
self::assertStringContainsString('lit/divi-colors-set', $unknown['hint']);
|
||||
$color = self::assertToolError($ability->execute(['variable' => 'gcid-primary-color']), 'unsupported');
|
||||
self::assertStringContainsString('nur deaktivieren', $color['hint']);
|
||||
self::assertArrayHasKey('variable', self::errorFields($ability->execute(['variable' => ' '])));
|
||||
$both = $ability->execute(['variable' => 'Eckenradius', 'preset' => self::LESETEXT]);
|
||||
self::assertSame('entweder preset oder variable angeben, nicht beides', self::errorFields($both)['variable']);
|
||||
}
|
||||
|
||||
public function testVariableDryRunListsPagesAndThePresetsThatUseIt(): void
|
||||
{
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
$pages->expects('preview')
|
||||
->with(
|
||||
Mockery::on(self::referencing(self::RADIUS)),
|
||||
Mockery::on(self::resolving(self::RADIUS, '8px')),
|
||||
null
|
||||
)
|
||||
->andReturn(PageRewriteReportTest::preview(1));
|
||||
$pages->expects('apply')->never();
|
||||
$ability = $this->ability($pages, self::readOnlyWriter(self::storedWithVariable()));
|
||||
|
||||
$dry = self::assertToolError($ability->execute(['variable' => 'eckenradius']), 'confirm_required');
|
||||
|
||||
self::assertSame(12, $dry['pages'][0]['post_id']);
|
||||
self::assertSame([['aaaaaaaaaa', 'changed']], array_map(
|
||||
static fn (array $row): array => [$row['id'], $row['action']],
|
||||
$dry['diff']
|
||||
));
|
||||
self::assertSame(PresetBook::fromDivi(self::storedWithVariable())->rev(), $dry['design_rev']);
|
||||
self::assertStringStartsWith('Entkoppeln von Variable „Eckenradius“ – betroffen', $dry['message']);
|
||||
self::assertStringContainsString('die Verweise in 1 Presets (diff, mit Snapshot)', $dry['message']);
|
||||
}
|
||||
|
||||
public function testAStringsVariableKeepsBeforeAndAfterAndArchivedOnesWork(): void
|
||||
{
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
$pages->expects('preview')
|
||||
->with(
|
||||
Mockery::on(self::referencing(self::CLAIM)),
|
||||
Mockery::on(self::resolving(self::CLAIM, '„Gut beraten“', ['before' => '„', 'after' => '“'])),
|
||||
null
|
||||
)
|
||||
->andReturn(PageRewriteReportTest::preview(1));
|
||||
|
||||
$dry = self::assertToolError($this->ability($pages)->execute(['variable' => self::CLAIM]), 'confirm_required');
|
||||
|
||||
self::assertArrayNotHasKey('diff', $dry, 'no preset uses the claim');
|
||||
}
|
||||
|
||||
public function testVariableConfirmResolvesThePresetsFirstThenThePages(): void
|
||||
{
|
||||
$calls = [];
|
||||
$stored = self::storedWithVariable();
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('presets')->andReturn($stored);
|
||||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||
$writer->expects('savePresets')
|
||||
->with(
|
||||
Mockery::on(static fn (array $rest): bool => ($rest['module'][0]['items'][0]['attrs']['button']
|
||||
['decoration']['border']['desktop']['value']['radius']['topLeft'] ?? null) === '8px'),
|
||||
false
|
||||
)
|
||||
->andReturnUsing(static function () use (&$calls, $stored): array {
|
||||
$calls[] = 'presets';
|
||||
|
||||
return PresetBook::fromDivi($stored)->withVariableResolved(self::RADIUS, '8px')->data();
|
||||
});
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->expects('save')->with('presets', $stored, 'lit/divi-design-detach')->andReturn('presets-5');
|
||||
$snapshots->expects('attachLog');
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
$pages->expects('apply')
|
||||
->with(
|
||||
'lit/divi-design-detach',
|
||||
Mockery::on(self::referencing(self::RADIUS)),
|
||||
Mockery::on(self::resolving(self::RADIUS, '8px')),
|
||||
null,
|
||||
'Entkoppeln von Variable „Eckenradius“'
|
||||
)
|
||||
->andReturnUsing(static function () use (&$calls): array {
|
||||
$calls[] = 'pages';
|
||||
|
||||
return PageRewriteReportTest::applied(1, 0);
|
||||
});
|
||||
$ability = $this->ability($pages, $writer, $snapshots, new FakeChangeLog(9));
|
||||
|
||||
$result = $ability->execute([
|
||||
'variable' => self::RADIUS,
|
||||
'confirm' => true,
|
||||
'design_rev' => PresetBook::fromDivi($stored)->rev(),
|
||||
]);
|
||||
|
||||
self::assertSame(['presets', 'pages'], $calls);
|
||||
self::assertSame('presets-5', $result['snapshot_id']);
|
||||
self::assertSame('aaaaaaaaaa', $result['diff'][0]['id']);
|
||||
self::assertStringStartsWith('Presets: 1 geändert.', $result['summary']);
|
||||
self::assertStringEndsWith(
|
||||
'Jetzt kann lit/divi-variables-set die Variable archivieren (status: archived).',
|
||||
$result['summary']
|
||||
);
|
||||
}
|
||||
|
||||
public function testVariablePostIdsSkipThePresetStep(): void
|
||||
{
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
$pages->expects('apply')
|
||||
->with('lit/divi-design-detach', Mockery::any(), Mockery::any(), [12], Mockery::type('string'))
|
||||
->andReturn(PageRewriteReportTest::applied(1, 0));
|
||||
$ability = $this->ability($pages, self::readOnlyWriter(self::storedWithVariable()));
|
||||
|
||||
$result = $ability->execute(['variable' => 'Eckenradius', 'post_ids' => [12], 'confirm' => true]);
|
||||
|
||||
self::assertSame([], $result['diff']);
|
||||
self::assertNull($result['snapshot_id']);
|
||||
self::assertStringNotContainsString('lit/divi-variables-set', $result['summary']);
|
||||
}
|
||||
|
||||
public function testNoVariableReferenceLeftPointsToArchiving(): void
|
||||
{
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
$pages->expects('preview')->andReturn(PageRewriteReportTest::preview(0));
|
||||
|
||||
$result = $this->ability($pages)->execute(['variable' => 'Eckenradius']);
|
||||
|
||||
self::assertSame(
|
||||
'Keine Verweise auf Variable „Eckenradius“ gefunden – nichts zu tun. Jetzt kann lit/divi-variables-set '
|
||||
. 'die Variable archivieren (status: archived).',
|
||||
$result['summary']
|
||||
);
|
||||
}
|
||||
|
||||
public function testFeaturesPerBranch(): void
|
||||
{
|
||||
$ability = $this->ability(self::untouched());
|
||||
|
||||
self::assertNotContains(DiviFeature::VariableWrite, $ability->diviFeatures(['preset' => self::LESETEXT]));
|
||||
self::assertContains(DiviFeature::VariableWrite, $ability->diviFeatures(['variable' => 'Eckenradius']));
|
||||
self::assertContains(DiviFeature::PresetWrite, $ability->diviFeatures(['variable' => 'Eckenradius']));
|
||||
}
|
||||
|
||||
public function testUnknownAndAmbiguousPresets(): void
|
||||
|
||||
Reference in New Issue
Block a user