fix: Variablen-Detach meldet verbleibende eingebettete Verweise und lehnt Werte mit Verweis, Markup oder leerem Inhalt ab
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
e951cc1353
commit
62f8465e60
@@ -88,10 +88,10 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
||||
}
|
||||
|
||||
/** Reads variables() (the Variablen-Verwaltung is allowed unless $manage is false), never saves. */
|
||||
private static function variableAdapter(bool $manage = true): GlobalVariablesAdapter&MockInterface
|
||||
private static function variableAdapter(bool $manage = true, string $radius = '8px'): GlobalVariablesAdapter&MockInterface
|
||||
{
|
||||
$variables = Mockery::mock(GlobalVariablesAdapter::class);
|
||||
$variables->allows('read')->andReturn(self::variables());
|
||||
$variables->allows('read')->andReturn(self::variables($radius));
|
||||
$variables->allows('canManage')->andReturn($manage);
|
||||
$variables->expects('save')->never();
|
||||
|
||||
@@ -99,11 +99,11 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, array<string, mixed>>> Global variables in Divi's stored form. */
|
||||
private static function variables(): array
|
||||
private static function variables(string $radius = '8px'): array
|
||||
{
|
||||
return [
|
||||
'numbers' => [self::RADIUS => [
|
||||
'id' => self::RADIUS, 'label' => 'Eckenradius', 'value' => '8px', 'order' => '1',
|
||||
'id' => self::RADIUS, 'label' => 'Eckenradius', 'value' => $radius, 'order' => '1',
|
||||
'status' => 'active', 'lastUpdated' => '2026-10-01T10:00:00.000Z', 'variableType' => 'numbers',
|
||||
]],
|
||||
// Archived or not does not matter for a detach (V9).
|
||||
@@ -294,6 +294,74 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, array{string, bool}> */
|
||||
public static function unsafeValues(): array
|
||||
{
|
||||
return [
|
||||
'empty' => ['', true],
|
||||
'marker' => ['$variable({"type":"content"})$', true],
|
||||
'end marker' => ['x)$', true],
|
||||
'markup without unfiltered_html' => ['<b>8px</b>', false],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider unsafeValues */
|
||||
public function testAValueThatMustNotBeInlinedRefusesTheCall(string $value, bool $unfiltered): void
|
||||
{
|
||||
$ability = $this->ability(
|
||||
self::untouched(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$unfiltered,
|
||||
self::variableAdapter(true, $value)
|
||||
);
|
||||
|
||||
$error = self::assertToolError($ability->execute(['variable' => 'Eckenradius']), 'unsupported');
|
||||
|
||||
self::assertStringContainsString('Divi-Builder', $error['hint']);
|
||||
}
|
||||
|
||||
public function testMarkupInTheValueIsAllowedWithUnfilteredHtml(): void
|
||||
{
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
$pages->expects('preview')->andReturn(PageRewriteReportTest::preview(1));
|
||||
$ability = $this->ability($pages, null, null, null, null, true, self::variableAdapter(true, '<b>8px</b>'));
|
||||
|
||||
self::assertToolError($ability->execute(['variable' => 'Eckenradius']), 'confirm_required');
|
||||
}
|
||||
|
||||
public function testReferencesLeftOnPagesNeverGetTheDoneHint(): void
|
||||
{
|
||||
$preview = PageRewriteReportTest::preview(0);
|
||||
$preview['skipped'] = [['post_id' => 12, 'reason' => 's0.r0.c0.m0: Die Variable steht hier eingebettet']];
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
$pages->expects('preview')->andReturn($preview);
|
||||
|
||||
$result = $this->ability($pages)->execute(['variable' => 'Eckenradius']);
|
||||
|
||||
self::assertStringNotContainsString('lit/divi-variables-set', $result['summary']);
|
||||
self::assertStringNotContainsString('Keine Verweise', $result['summary']);
|
||||
self::assertSame($preview['skipped'], $result['skipped']);
|
||||
}
|
||||
|
||||
public function testEmbeddedReferencesInPresetsAreReportedAndWithholdTheDoneHint(): void
|
||||
{
|
||||
$stored = PresetBookTest::stored();
|
||||
$stored['module']['divi/button']['items']['aaaaaaaaaa']['attrs'] = ['button' => ['decoration' => [
|
||||
'border' => ['desktop' => ['value' => ['radius' => 'Ab ' . VariableDetachRewriteTest::ref(self::RADIUS) . ' x']]],
|
||||
]]];
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
$pages->expects('preview')->andReturn(PageRewriteReportTest::preview(0));
|
||||
|
||||
$result = $this->ability($pages, self::readOnlyWriter($stored))->execute(['variable' => 'Eckenradius']);
|
||||
|
||||
self::assertStringNotContainsString('lit/divi-variables-set', $result['summary']);
|
||||
self::assertStringContainsString('In 1 Presets steht die Variable eingebettet', $result['warnings'][0]);
|
||||
self::assertStringContainsString('CTA primär', $result['warnings'][0]);
|
||||
}
|
||||
|
||||
public function testVariablePostIdsSkipThePresetStep(): void
|
||||
{
|
||||
$pages = Mockery::mock(PageRewriter::class);
|
||||
|
||||
@@ -9,6 +9,8 @@ use Closure;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Json;
|
||||
use LIT\MCP\Integrations\Divi\Design\VariableDetachRewrite;
|
||||
use LIT\MCP\Integrations\Divi\Design\NodeRewrite;
|
||||
use LIT\MCP\Integrations\Divi\Design\PageRewriter;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
@@ -368,6 +370,33 @@ final class PageRewriterTest extends TestCase
|
||||
self::assertSame(['102'], array_column($this->log->records, 'object_id'));
|
||||
}
|
||||
|
||||
/** V9: a page that only mentions the variable inside a longer text is not saved, but reported as skipped. */
|
||||
public function testVariableReferencesLeftBehindAreSkippedEvenOnAPageThatIsNotSaved(): void
|
||||
{
|
||||
$embedded = '<!-- wp:divi/text ' . Json::encode(['content' => ['innerContent' => ['desktop' => ['value' => 'Ab '
|
||||
. VariableDetachRewriteTest::ref('gvid-radius0001') . ' Rundung']]]]) . ' /-->';
|
||||
$whole = '<!-- wp:divi/text ' . Json::encode(['content' => ['innerContent' => ['desktop' => ['value'
|
||||
=> VariableDetachRewriteTest::ref('gvid-radius0001')]]]]) . ' /-->';
|
||||
$rewriter = $this->rewriter([
|
||||
self::page(101, self::layout($embedded)),
|
||||
self::page(102, self::layout($whole, $embedded)),
|
||||
]);
|
||||
$rewrite = static fn (): NodeRewrite => new VariableDetachRewrite('gvid-radius0001', '8px', 'numbers');
|
||||
|
||||
$preview = $rewriter->preview(self::query(), $rewrite, null);
|
||||
$result = $rewriter->apply(self::ABILITY, self::query(), $rewrite, null, self::SUMMARY);
|
||||
|
||||
self::assertSame(1, $preview['pages_count']);
|
||||
self::assertSame(
|
||||
[101, 102],
|
||||
array_column($preview['skipped'], 'post_id')
|
||||
);
|
||||
self::assertStringStartsWith('s0.r0.c0.m0: Die Variable steht hier eingebettet', $preview['skipped'][0]['reason']);
|
||||
self::assertStringStartsWith('s0.r0.c0.m1: ', $preview['skipped'][1]['reason']);
|
||||
self::assertSame([102], array_column($result['saved'], 'post_id'));
|
||||
self::assertSame([101, 102], array_column($result['skipped'], 'post_id'));
|
||||
}
|
||||
|
||||
/** R8: pages loadWritable() refuses (Divi 4, unparsable – fail-closed candidates) are skipped, not remaining. */
|
||||
public function testRefusedPagesAreSkippedWithTheLoaderMessage(): void
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Design\NodeRewrite;
|
||||
use LIT\MCP\Integrations\Divi\Design\TreeRewrite;
|
||||
use LIT\MCP\Integrations\Divi\Design\VariableDetachRewrite;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
|
||||
final class TreeRewriteTest extends TestCase
|
||||
@@ -134,4 +135,22 @@ final class TreeRewriteTest extends TestCase
|
||||
$result['skipped']
|
||||
);
|
||||
}
|
||||
|
||||
public function testAReportsLeftoversStrategyListsUnchangedBlocksAsSkipped(): void
|
||||
{
|
||||
$embedded = 'Ab ' . VariableDetachRewriteTest::ref('gvid-radius0001') . ' Rundung';
|
||||
$text = static fn (string $value): string => '<!-- wp:divi/text ' . \LIT\MCP\Integrations\Divi\BlockTree\Json::encode(
|
||||
['content' => ['innerContent' => ['desktop' => ['value' => $value]]]]
|
||||
) . ' /-->';
|
||||
$document = self::document(
|
||||
$text($embedded) . $text(VariableDetachRewriteTest::ref('gvid-radius0001')) . '<!-- wp:divi/text /-->'
|
||||
);
|
||||
|
||||
$result = (new TreeRewrite(new VariableDetachRewrite('gvid-radius0001', '8px', 'numbers')))
|
||||
->apply(NodeIndex::build($document));
|
||||
|
||||
self::assertSame(['s0.r0.c0.m1'], array_column($result['changed'], 'id'));
|
||||
self::assertSame(['s0.r0.c0.m0'], array_column($result['skipped'], 'id'), 'embedded only: reported');
|
||||
self::assertSame([], $result['warnings']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,19 +148,42 @@ final class VariableDetachRewriteTest extends TestCase
|
||||
$attrs = $rewrite->rewrite($first);
|
||||
self::assertSame($embedded, $attrs['content']['innerContent']['desktop']['value'] ?? null);
|
||||
$first->setAttrs((array) $attrs);
|
||||
self::assertSame(
|
||||
['s0.r0.c0.m0: Die Variable steht hier eingebettet in einem längeren Text; dieser Verweis bleibt stehen – '
|
||||
. 'im Divi-Builder ersetzen.'],
|
||||
$rewrite->notes($first, 's0.r0.c0.m0')
|
||||
);
|
||||
self::assertStringContainsString('eingebettet', (string) $rewrite->leftover($first));
|
||||
self::assertStringContainsString('bleibt unverändert', (string) $rewrite->leftover($first));
|
||||
self::assertNotNull($rewrite->leftover(self::node($index, 'm1')), 'embedded only: still reported');
|
||||
self::assertNull($rewrite->rewrite(self::node($index, 'm1')), 'only embedded: nothing to replace');
|
||||
self::assertSame(
|
||||
[],
|
||||
(new VariableDetachRewrite(self::CLAIM, 'x', 'strings'))->notes($first, 's0.r0.c0.m0'),
|
||||
'another variable is not noted'
|
||||
self::assertNull(
|
||||
(new VariableDetachRewrite(self::CLAIM, 'x', 'strings'))->leftover($first),
|
||||
'another variable is not reported'
|
||||
);
|
||||
}
|
||||
|
||||
public function testUnsafeBeforeAndAfterLeaveTheBlockWholeAndReportIt(): void
|
||||
{
|
||||
$index = self::index([
|
||||
['divi/text', ['content' => ['innerContent' => self::desktop(self::ref(self::CLAIM, ['before' => '<b>']))]]],
|
||||
['divi/text', ['content' => ['innerContent' => self::desktop(
|
||||
// Built by hand: VariableRef::serialize() itself refuses a marker in the settings.
|
||||
'$variable(' . Json::encode(['type' => 'content', 'value' => [
|
||||
'name' => self::CLAIM, 'settings' => ['after' => 'a $variable(b']
|
||||
]]) . ')$'
|
||||
)]]],
|
||||
['divi/text', ['content' => ['innerContent' => self::desktop(self::ref(self::CLAIM, ['before' => '<b>']))]]],
|
||||
]);
|
||||
$strict = new VariableDetachRewrite(self::CLAIM, 'x', 'strings');
|
||||
$allowed = new VariableDetachRewrite(self::CLAIM, 'x', 'strings', true);
|
||||
|
||||
self::assertNull($strict->rewrite(self::node($index, 'm0')));
|
||||
self::assertStringContainsString('Markup', (string) $strict->leftover(self::node($index, 'm0')));
|
||||
self::assertNull($allowed->rewrite(self::node($index, 'm1')), 'a marker is never inlined');
|
||||
self::assertNotNull($allowed->leftover(self::node($index, 'm1')));
|
||||
$third = self::node($index, 'm2');
|
||||
$attrs = (array) $allowed->rewrite($third);
|
||||
self::assertSame('<b>x', $attrs['content']['innerContent']['desktop']['value'] ?? null);
|
||||
$third->setAttrs($attrs);
|
||||
self::assertNull($allowed->leftover($third));
|
||||
}
|
||||
|
||||
public function testInstancesThatMentionTheIdConcernTheRewrite(): void
|
||||
{
|
||||
$index = self::index([
|
||||
|
||||
Reference in New Issue
Block a user