feat: Fundstellen aller Module eines Typs, Standard-Gruppen-Presets in der Fundstellensuche (ModuleUsageQuery)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 16:55:54 +02:00
co-authored by Claude Sonnet 5.5
parent 1f9b50f1a6
commit 763d765660
11 changed files with 329 additions and 23 deletions
@@ -127,7 +127,10 @@ final class DiviPresetGetAbility implements Ability
$attrs = is_array($item['attrs'] ?? null) ? $item['attrs'] : []; $attrs = is_array($item['attrs'] ?? null) ? $item['attrs'] : [];
$module = is_string($item['moduleName'] ?? null) ? $item['moduleName'] : $scope->name; $module = is_string($item['moduleName'] ?? null) ? $item['moduleName'] : $scope->name;
$usage = $this->usages->reportFor( $usage = $this->usages->reportFor(
new PresetUsageQuery([['scope' => $scope, 'id' => $id, 'default' => $isDefault]]), PresetUsageQuery::withGroups(
[['scope' => $scope, 'id' => $id, 'default' => $isDefault]],
$this->writer->groupsOf(...)
),
false false
); );
@@ -17,7 +17,7 @@ defined('ABSPATH') || exit;
/** /**
* lit/divi-presets-delete (spec §6.4, G5): deletes presets nothing uses any more – no page, library entry or Theme * lit/divi-presets-delete (spec §6.4, G5): deletes presets nothing uses any more – no page, library entry or Theme
* Builder layout (GlobalUsages with PresetUsageQuery), and no other preset that nests it. A used preset is refused * Builder layout (GlobalUsages with PresetUsageQuery), and no other preset that nests it. A used preset is refused
* with its usages (in_use); lit/divi-design-detach (Etappe 4b) removes the references first. Only explicit references * with its usages (in_use); lit/divi-design-detach removes the references first. Only explicit references
* block: a default preset loses its default, and the posts with modules that use it implicitly are listed in the dry * block: a default preset loses its default, and the posts with modules that use it implicitly are listed in the dry
* run (spec §6.4). Through the change pipeline with Divi's delete semantics (legacy option cleaned). * run (spec §6.4). Through the change pipeline with Divi's delete semantics (legacy option cleaned).
*/ */
@@ -25,8 +25,8 @@ final class DiviPresetsDeleteAbility implements Ability
{ {
public const MAX_ITEMS = 20; public const MAX_ITEMS = 20;
private const DETACH_HINT = 'Erst die Verweise entfernen: lit/divi-design-detach (ab Etappe 4b) oder im ' private const DETACH_HINT = 'Erst die Verweise entfernen: lit/divi-design-detach oder im Divi-Builder; '
. 'Divi-Builder; danach erneut löschen.'; . 'danach erneut löschen.';
/** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */ /** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */
private const DEFAULT_WARNING = 'Module ohne eigenes Preset verwenden danach das Divi-Standardaussehen.'; private const DEFAULT_WARNING = 'Module ohne eigenes Preset verwenden danach das Divi-Standardaussehen.';
@@ -179,12 +179,13 @@ final class DiviPresetsDeleteAbility implements Ability
$diff = PresetDiff::between($before, $book); $diff = PresetDiff::between($before, $book);
$defaults = array_values(array_filter($targets, static fn (array $target): bool => $target['default'])); $defaults = array_values(array_filter($targets, static fn (array $target): bool => $target['default']));
$groupsOf = $this->writer->groupsOf(...);
return new DesignPlan( return new DesignPlan(
$book->data(), $book->data(),
$diff, $diff,
PresetDiff::summary($diff), PresetDiff::summary($diff),
$defaults === [] ? null : $this->usages->reportFor(new PresetUsageQuery($defaults)), $defaults === [] ? null : $this->usages->reportFor(PresetUsageQuery::withGroups($defaults, $groupsOf)),
$defaults === [] ? [] : [self::DEFAULT_WARNING], $defaults === [] ? [] : [self::DEFAULT_WARNING],
true true
); );
@@ -20,7 +20,8 @@ defined('ABSPATH') || exit;
* lit/divi-presets-set (spec §6.3, G7): creates and changes module and option group presets – from the short and full * lit/divi-presets-set (spec §6.3, G7): creates and changes module and option group presets – from the short and full
* form, from a module of a page (from_node), with nested group presets and as the default preset – through the * form, from a module of a page (from_node), with nested group presets and as the default preset – through the
* change pipeline (dry run, design_rev, snapshot, Divi's sync, change log). New names must be free within the * change pipeline (dry run, design_rev, snapshot, Divi's sync, change log). New names must be free within the
* module/group (A5). The dry run lists the posts that use a changed preset or reach a new default. * module/group (A5). The dry run lists the posts that use a changed preset or reach a new default, also implicitly
* (ruling R11).
*/ */
final class DiviPresetsSetAbility implements Ability final class DiviPresetsSetAbility implements Ability
{ {
@@ -33,9 +34,6 @@ final class DiviPresetsSetAbility implements Ability
private const MARKUP = 'Vorhandene Presets enthalten HTML/CSS, das beim Speichern ohne das Recht unfiltered_html ' private const MARKUP = 'Vorhandene Presets enthalten HTML/CSS, das beim Speichern ohne das Recht unfiltered_html '
. 'verloren ginge; nichts wurde gespeichert.'; . 'verloren ginge; nichts wurde gespeichert.';
private const GROUP_DEFAULT_WARNING = 'Standard-Gruppen-Presets wirken zusätzlich auf alle Module mit dieser '
. 'Gruppe ohne eigenes Gruppen-Preset; diese Seiten sind nicht aufgelistet.';
/** /**
* @param Closure(): PresetComposer $composer * @param Closure(): PresetComposer $composer
* @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). * @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights).
@@ -159,7 +157,7 @@ final class DiviPresetsSetAbility implements Ability
$invalid = []; $invalid = [];
$targets = []; $targets = [];
$created = []; $created = [];
$warnings = []; $groupsOf = $this->writer->groupsOf(...);
foreach ($items as $index => $item) { foreach ($items as $index => $item) {
$field = sprintf('items[%d]', $index); $field = sprintf('items[%d]', $index);
if (! is_array($item)) { if (! is_array($item)) {
@@ -184,9 +182,6 @@ final class DiviPresetsSetAbility implements Ability
$book = $book->with($scope, $result['item']); $book = $book->with($scope, $result['item']);
if (($item['make_default'] ?? null) === true) { if (($item['make_default'] ?? null) === true) {
$book = $book->withDefault($scope, $id); $book = $book->withDefault($scope, $id);
if ($scope->kind === PresetScope::GROUP) {
$warnings[] = self::GROUP_DEFAULT_WARNING;
}
} elseif (($item['make_default'] ?? null) === false && $book->defaultId($scope) === $id) { } elseif (($item['make_default'] ?? null) === false && $book->defaultId($scope) === $id) {
$book = $book->withDefault($scope, ''); $book = $book->withDefault($scope, '');
} }
@@ -214,8 +209,7 @@ final class DiviPresetsSetAbility implements Ability
$book->data(), $book->data(),
$diff, $diff,
PresetDiff::summary($diff), PresetDiff::summary($diff),
$targets === [] ? null : $this->usages->reportFor(new PresetUsageQuery($targets)), $targets === [] ? null : $this->usages->reportFor(PresetUsageQuery::withGroups($targets, $groupsOf))
array_values(array_unique($warnings))
); );
} }
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Library\UsageQuery;
defined('ABSPATH') || exit;
/**
* The posts with a module of one type (GlobalUsages::reportFor(), Etappe 4b): candidates contain the block name, an
* escaped slash or a JSON escape of a letter (a global instance keeps its type as "blockName" in JSON, fail closed
* like ruling P23); matches() parses the content. Global instances of the type (opaque, never rewritten) count, so
* the page rewriter reports them as skipped; so does markup the parser rejects.
*/
final class ModuleUsageQuery implements UsageQuery
{
private const NAME = '/^([a-z][a-z0-9_-]*)\/([a-z][a-z0-9_-]*)$/D';
private const ESCAPED_LETTER = '\\\\u00[4-7][0-9a-fA-F]';
/** @throws InvalidArgumentException When $module is no full block name such as "divi/button". */
public function __construct(private string $module)
{
if (preg_match(self::NAME, $module) !== 1) {
throw new InvalidArgumentException('ModuleUsageQuery needs a full block name such as divi/button.');
}
}
public function candidateCondition(\wpdb $db): string
{
return (string) $db->prepare(
'(post_content LIKE %s OR post_content REGEXP %s OR post_content REGEXP %s)',
'%' . $db->esc_like($this->module) . '%',
$this->escapedSlash(),
self::ESCAPED_LETTER
);
}
public function matches(string $content): bool
{
try {
$index = NodeIndex::build((new Parser())->parse($content));
} catch (MalformedMarkup) {
return true;
}
foreach ($index->entries() as $entry) {
if (GlobalInstance::effectiveName($entry['block']) === $this->module) {
return true;
}
}
return false;
}
/** Regular expression for "divi\/button" and "divi/button" (two literal backslashes in the pattern). */
private function escapedSlash(): string
{
[$namespace, $name] = explode('/', $this->module, 2);
return $namespace . '(\\\\/|\\\\u002[fF])' . $name;
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design; namespace LIT\MCP\Integrations\Divi\Design;
use Closure;
use LIT\MCP\Integrations\Divi\BlockTree\Block; use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup; use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex; use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
@@ -17,15 +18,20 @@ defined('ABSPATH') || exit;
* - module preset $id: a block of the module with $id in its modulePreset stack (array or legacy string); * - module preset $id: a block of the module with $id in its modulePreset stack (array or legacy string);
* the default preset additionally reaches every block of the module without an own preset ("" / "default" / * the default preset additionally reaches every block of the module without an own preset ("" / "default" /
* "_initial" mean the default, GlobalPreset::is_preset_id_as_default); * "_initial" mean the default, GlobalPreset::is_preset_id_as_default);
* - group preset $id: a block with $id in groupPreset.<attr>.presetId whose groupName is the group; * - group preset $id: a block with $id in groupPreset.<attr>.presetId whose groupName is the group; the default
* group preset additionally reaches every block whose module has the group (groupsOf) and that has no own group
* preset for it (ruling R11) – without $groupsOf only the explicit references are found;
* - markup the parser rejects counts as a use. * - markup the parser rejects counts as a use.
* Global instances (opaque) get their content from the library entry, so they never reach a default preset.
* Pure PHP. * Pure PHP.
*/ */
final class PresetUsageMatcher final class PresetUsageMatcher
{ {
private const DEFAULT_IDS = ['', 'default', '_initial']; /** Preset IDs that mean "no own preset" (GlobalPreset::is_preset_id_as_default). */
public const DEFAULT_IDS = ['', 'default', '_initial'];
public function __construct(private Parser $parser = new Parser()) /** @param (Closure(string $module): array<string, string>)|null $groupsOf groupId => groupName of a module */
public function __construct(private Parser $parser = new Parser(), private ?Closure $groupsOf = null)
{ {
} }
@@ -37,8 +43,9 @@ final class PresetUsageMatcher
return true; return true;
} }
$groups = [];
foreach ($index->entries() as $entry) { foreach ($index->entries() as $entry) {
if ($this->usesIn($entry['block'], $scope, $id, $isDefault)) { if ($this->usesIn($entry['block'], $scope, $id, $isDefault, $groups)) {
return true; return true;
} }
} }
@@ -46,13 +53,15 @@ final class PresetUsageMatcher
return false; return false;
} }
private function usesIn(Block $block, PresetScope $scope, string $id, bool $isDefault): bool /** @param array<string, array<string, string>> $groups groupsOf() per module name, filled on demand */
private function usesIn(Block $block, PresetScope $scope, string $id, bool $isDefault, array &$groups): bool
{ {
if ($scope->kind === PresetScope::MODULE) { if ($scope->kind === PresetScope::MODULE) {
return $this->usesModulePreset($block, $scope, $id, $isDefault); return $this->usesModulePreset($block, $scope, $id, $isDefault);
} }
return $this->usesGroupPreset($block->attrs(), $scope, $id); return $this->usesGroupPreset($block->attrs(), $scope, $id)
|| ($isDefault && $this->reachesByDefault($block, $scope, $groups));
} }
private function usesModulePreset(Block $block, PresetScope $scope, string $id, bool $isDefault): bool private function usesModulePreset(Block $block, PresetScope $scope, string $id, bool $isDefault): bool
@@ -84,4 +93,37 @@ final class PresetUsageMatcher
return false; return false;
} }
/**
* Ruling R11: the module of $block has the group $scope and $block has no own group preset for its groupId.
*
* @param array<string, array<string, string>> $groups
*/
private function reachesByDefault(Block $block, PresetScope $scope, array &$groups): bool
{
if ($this->groupsOf === null || ! $block->isDivi() || $block->isOpaque()) {
return false;
}
$groups[$block->name()] ??= ($this->groupsOf)($block->name());
$own = is_array($block->attrs()['groupPreset'] ?? null) ? $block->attrs()['groupPreset'] : [];
foreach ($groups[$block->name()] as $groupId => $groupName) {
if ($groupName === $scope->name && ! self::hasOwnPreset($own[$groupId] ?? null)) {
return true;
}
}
return false;
}
private static function hasOwnPreset(mixed $entry): bool
{
if (! is_array($entry)) {
return false;
}
$ids = array_filter((array) ($entry['presetId'] ?? []), 'is_string');
return array_diff($ids, self::DEFAULT_IDS) !== [];
}
} }
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design; namespace LIT\MCP\Integrations\Divi\Design;
use Closure;
use InvalidArgumentException; use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Library\UsageQuery; use LIT\MCP\Integrations\Divi\Library\UsageQuery;
@@ -11,8 +12,9 @@ defined('ABSPATH') || exit;
/** /**
* The posts that use any of the presets $targets (GlobalUsages::reportFor()): candidates contain one of the IDs, a * The posts that use any of the presets $targets (GlobalUsages::reportFor()): candidates contain one of the IDs, a
* JSON escape of a letter (an escaped ID or key, fail closed like ruling P23) or – for a default module preset – a * JSON escape of a letter (an escaped ID or key, fail closed like ruling P23), for a default module preset a block
* block of the module; PresetUsageMatcher decides per target. * of the module and for a default group preset any Divi block (ruling R11: which modules have the group is only
* known to PresetUsageMatcher::$groupsOf); PresetUsageMatcher decides per target.
*/ */
final class PresetUsageQuery implements UsageQuery final class PresetUsageQuery implements UsageQuery
{ {
@@ -33,6 +35,17 @@ final class PresetUsageQuery implements UsageQuery
$this->targets = array_values($targets); $this->targets = array_values($targets);
} }
/**
* Targets whose default group presets are also found where a module reaches them implicitly.
*
* @param list<array{scope: PresetScope, id: string, default: bool}> $targets
* @param Closure(string $module): array<string, string> $groupsOf groupId => groupName of a module
*/
public static function withGroups(array $targets, Closure $groupsOf): self
{
return new self($targets, new PresetUsageMatcher(groupsOf: $groupsOf));
}
public function candidateCondition(\wpdb $db): string public function candidateCondition(\wpdb $db): string
{ {
$likes = []; $likes = [];
@@ -40,6 +53,8 @@ final class PresetUsageQuery implements UsageQuery
$likes[] = '%' . $db->esc_like($target['id']) . '%'; $likes[] = '%' . $db->esc_like($target['id']) . '%';
if ($target['default'] && $target['scope']->kind === PresetScope::MODULE) { if ($target['default'] && $target['scope']->kind === PresetScope::MODULE) {
$likes[] = '%' . $db->esc_like('wp:' . $target['scope']->name . ' ') . '%'; $likes[] = '%' . $db->esc_like('wp:' . $target['scope']->name . ' ') . '%';
} elseif ($target['default']) {
$likes[] = '%' . $db->esc_like('wp:divi/') . '%';
} }
} }
$likes = array_values(array_unique($likes)); $likes = array_values(array_unique($likes));
@@ -98,6 +98,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
self::assertSame(3, $error['usages'][0]['post_id']); self::assertSame(3, $error['usages'][0]['post_id']);
self::assertStringContainsString('lit/divi-design-detach', $error['hint']); self::assertStringContainsString('lit/divi-design-detach', $error['hint']);
self::assertStringNotContainsString('4b', $error['hint'], 'the tool exists now');
} }
public function testNestedUseInAnotherPresetCounts(): void public function testNestedUseInAnotherPresetCounts(): void
@@ -138,6 +138,16 @@ final class DiviPresetsSetAbilityTest extends TestCase
self::assertSame(['changed', 'default_set'], array_column($dry['diff'], 'action')); self::assertSame(['changed', 'default_set'], array_column($dry['diff'], 'action'));
} }
/** R11: a default group preset no longer warns that pages are "not listed" – the dry run lists them. */
public function testDefaultGroupPresetHasNoNotListedWarning(): void
{
$dry = self::assertToolError($this->ability()->execute(['items' => [[
'group' => 'font-body', 'preset' => 'Lesetext', 'rename' => 'Lesetext neu', 'make_default' => true,
]]]), 'confirm_required');
self::assertSame([], $dry['warnings']);
}
public function testUnknownPresetAndBadItemsAreCollected(): void public function testUnknownPresetAndBadItemsAreCollected(): void
{ {
$result = self::assertToolError($this->ability()->execute(['items' => [ $result = self::assertToolError($this->ability()->execute(['items' => [
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Design\ModuleUsageQuery;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class ModuleUsageQueryTest extends TestCase
{
private static function page(string $inner): string
{
return '<!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->' . $inner
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->';
}
public function testCandidatesContainTheNameAnEscapedSlashOrAnEscapedLetter(): void
{
$db = Mockery::mock(\wpdb::class);
$db->allows('esc_like')->andReturnUsing(static fn (string $text): string => $text);
$db->expects('prepare')
->with(
'(post_content LIKE %s OR post_content REGEXP %s OR post_content REGEXP %s)',
'%divi/button%',
'divi(\\\\/|\\\\u002[fF])button',
'\\\\u00[4-7][0-9a-fA-F]'
)
->andReturn('(prepared)');
self::assertSame('(prepared)', (new ModuleUsageQuery('divi/button'))->candidateCondition($db));
}
public function testNeedsAFullBlockName(): void
{
$this->expectException(InvalidArgumentException::class);
new ModuleUsageQuery('button');
}
public function testMatchesBlocksOfTheModule(): void
{
$query = new ModuleUsageQuery('divi/button');
self::assertTrue($query->matches(self::page('<!-- wp:divi/button {"a":1} /-->')));
self::assertTrue($query->matches(self::page('<!-- wp:divi/button /-->')));
self::assertTrue($query->matches(self::page(
'<!-- wp:divi/button {"a":1} --><!-- wp:divi/text /--><!-- /wp:divi/button -->'
)));
self::assertFalse($query->matches(self::page('<!-- wp:divi/text {"a":1} /-->')));
self::assertFalse($query->matches(self::page('<!-- wp:divi/button-group /-->')), 'another module name');
self::assertFalse($query->matches('<p>Kein Divi-Inhalt</p>'));
}
public function testGlobalInstancesOfTheModuleCountSoThatTheyAreReportedAsSkipped(): void
{
$query = new ModuleUsageQuery('divi/button');
self::assertTrue($query->matches(self::page(
'<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi/button"} /-->'
)));
self::assertTrue($query->matches(self::page(
'<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi\/button"} /-->'
)), 'an escaped slash decodes to the same name');
self::assertFalse($query->matches(self::page(
'<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi/text"} /-->'
)));
self::assertTrue($query->matches(self::page(
'<!-- wp:divi/button {"globalModule":"1058"} /-->'
)), 'Divi 4 conversion: the block itself carries globalModule');
self::assertTrue($query->matches(self::page('<!-- wp:divi/button {"a":1,} /-->')), 'unreadable attributes');
}
public function testBrokenMarkupCountsAsUse(): void
{
self::assertTrue((new ModuleUsageQuery('divi/button'))->matches(
'<!-- wp:divi/section {"a": --><!-- /wp:divi/row -->'
));
}
}
@@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase;
final class PresetUsageMatcherTest extends TestCase final class PresetUsageMatcherTest extends TestCase
{ {
private const TEXT = '<!-- wp:divi/section --><!-- wp:divi/text %s /--><!-- /wp:divi/section -->';
private const BUTTON = '<!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->' private const BUTTON = '<!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
. '<!-- wp:divi/button %s /--><!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->'; . '<!-- wp:divi/button %s /--><!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->';
@@ -62,4 +63,58 @@ final class PresetUsageMatcherTest extends TestCase
false false
)); ));
} }
private static function groupsOf(string $module): array
{
return $module === 'divi/text' ? ['content.decoration.bodyFont' => 'divi/font-body'] : [];
}
public function testDefaultGroupPresetReachesModulesWithTheGroup(): void
{
$matcher = new PresetUsageMatcher(groupsOf: self::groupsOf(...));
$group = new PresetScope('group', 'divi/font-body');
$own = '{"groupPreset":{"content.decoration.bodyFont":{"presetId":["%s"],"groupName":"divi/font-body"}}}';
self::assertTrue($matcher->uses(sprintf(self::TEXT, '{}'), $group, 'gggggggggg', true));
self::assertTrue(
$matcher->uses(sprintf(self::TEXT, sprintf($own, 'default')), $group, 'gggggggggg', true),
'an entry with the default ID is no own preset'
);
self::assertFalse(
$matcher->uses(sprintf(self::TEXT, sprintf($own, 'hhhhhhhhhh')), $group, 'gggggggggg', true),
'an own group preset replaces the default'
);
self::assertFalse(
$matcher->uses(self::page('{}'), $group, 'gggggggggg', true),
'a button has no font-body group'
);
self::assertFalse(
$matcher->uses(sprintf(self::TEXT, '{}'), new PresetScope('group', 'divi/spacing'), 'gggggggggg', true),
'the text module has no spacing group in this fixture'
);
self::assertFalse(
$matcher->uses(sprintf(self::TEXT, '{}'), $group, 'gggggggggg', false),
'a preset that is not the default needs an explicit entry'
);
}
public function testDefaultGroupPresetIsNotDetectedWithoutGroupsOf(): void
{
self::assertFalse((new PresetUsageMatcher())->uses(
sprintf(self::TEXT, '{}'),
new PresetScope('group', 'divi/font-body'),
'gggggggggg',
true
));
}
public function testGlobalInstancesDoNotReachADefaultGroupPreset(): void
{
$matcher = new PresetUsageMatcher(groupsOf: static fn (string $module): array => [
'content.decoration.bodyFont' => 'divi/font-body',
]);
$instance = '<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi/text"} /-->';
self::assertFalse($matcher->uses($instance, new PresetScope('group', 'divi/font-body'), 'gggggggggg', true));
}
} }
@@ -62,4 +62,37 @@ final class PresetUsageQueryTest extends TestCase
new PresetUsageQuery([]); new PresetUsageQuery([]);
} }
public function testADefaultGroupPresetLooksForEveryDiviBlock(): void
{
$db = Mockery::mock(\wpdb::class);
$db->allows('esc_like')->andReturnUsing(static fn (string $text): string => $text);
$db->expects('prepare')
->with(
'(post_content LIKE %s OR post_content LIKE %s OR post_content REGEXP %s)',
'%gggggggggg%',
'%wp:divi/%',
'\\\\u00[4-7][0-9a-fA-F]'
)
->andReturn('(prepared)');
$query = new PresetUsageQuery([
['scope' => new PresetScope('group', 'divi/font-body'), 'id' => 'gggggggggg', 'default' => true],
]);
self::assertSame('(prepared)', $query->candidateCondition($db));
}
public function testWithGroupsDetectsTheImplicitUseOfADefaultGroupPreset(): void
{
$query = PresetUsageQuery::withGroups(
[['scope' => new PresetScope('group', 'divi/font-body'), 'id' => 'gggggggggg', 'default' => true]],
static fn (string $module): array => $module === 'divi/text'
? ['content.decoration.bodyFont' => 'divi/font-body']
: []
);
self::assertTrue($query->matches('<!-- wp:divi/text {} /-->'));
self::assertFalse($query->matches('<!-- wp:divi/button {} /-->'));
}
} }