feat: {{link:ID}} placeholder via Divi's post_link_url dynamic content
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Compose;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Entries {{link:<ID>}} may point to (E13a). Pure interface – WpLinkTargets asks WordPress.
|
||||
*/
|
||||
interface LinkTargets
|
||||
{
|
||||
/** Post type of entry $id when it may be linked (exists, not trashed, readable); null otherwise. */
|
||||
public function postTypeOf(int $id): ?string;
|
||||
}
|
||||
@@ -12,10 +12,10 @@ defined('ABSPATH') || exit;
|
||||
/**
|
||||
* Translates the placeholders of spec §5.4 into Divi's $variable(…)$ syntax:
|
||||
* {{color:primary|<Name>|gcid-…}}, {{var:<Name>|gvid-…}}, {{font:heading|body}},
|
||||
* {{post:title|…}}, {{field:<meta_key>}}. A placeholder must be the whole value (D9); unknown
|
||||
* names fail with the list of existing ones, labels two entries share fail with their IDs
|
||||
* (ruling S13a, DesignSystem::colorIdFor/variableFor). The result is still checked by VariableRef
|
||||
* wherever it is written. Pure PHP.
|
||||
* {{post:title|…}}, {{field:<meta_key>}}, {{link:<ID>}} (internal link, URL fields only – E13a).
|
||||
* A placeholder must be the whole value (D9); unknown names fail with the list of existing ones,
|
||||
* labels two entries share fail with their IDs (ruling S13a, DesignSystem::colorIdFor/variableFor).
|
||||
* The result is still checked by VariableRef wherever it is written. Pure PHP.
|
||||
*/
|
||||
final class Placeholders
|
||||
{
|
||||
@@ -37,9 +37,10 @@ final class Placeholders
|
||||
'post_title', 'post_excerpt', 'post_date', 'post_modified_date', 'post_author', 'post_categories', 'post_tags',
|
||||
];
|
||||
|
||||
private const PATTERN = '/^\{\{\s*(color|var|font|post|field)\s*:\s*([^{}]+?)\s*\}\}$/';
|
||||
private const PATTERN = '/^\{\{\s*(color|var|font|post|field|link)\s*:\s*([^{}]+?)\s*\}\}$/';
|
||||
|
||||
public function __construct(private DesignSystem $design)
|
||||
/** @param LinkTargets|null $links Entries {{link:…}} may point to; without them every link fails. */
|
||||
public function __construct(private DesignSystem $design, private ?LinkTargets $links = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ final class Placeholders
|
||||
if (str_contains($value, '{{')) {
|
||||
throw new InvalidArgumentException(
|
||||
'Platzhalter müssen allein im Wert stehen, z. B. "{{color:primary}}" (bekannt: color, var, font, '
|
||||
. 'post, field).'
|
||||
. 'post, field, link).'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,6 +69,7 @@ final class Placeholders
|
||||
'var' => $this->variable($name),
|
||||
'font' => $this->font($name),
|
||||
'post' => $this->post($name),
|
||||
'link' => $this->link($name),
|
||||
default => $this->field($name),
|
||||
};
|
||||
}
|
||||
@@ -152,6 +154,36 @@ final class Placeholders
|
||||
return VariableRef::serialize('content', $option, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* {{link:<ID>}} → Divi's own internal link post_link_url_<type> with settings.post_id (F27):
|
||||
* rendered as get_permalink() at view time, so a draft target gets its pretty URL once published.
|
||||
* Whether Divi offers that option (public post types only) VariableRef checks against the registry;
|
||||
* that the value goes into a URL field and the target is readable (S37) the choke point checks again.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function link(string $name): string
|
||||
{
|
||||
// FILTER_VALIDATE_INT: no id beyond PHP_INT_MAX that would saturate to another entry.
|
||||
$id = preg_match('/^[1-9][0-9]{0,18}$/D', $name) === 1 ? filter_var($name, FILTER_VALIDATE_INT) : false;
|
||||
if (! is_int($id)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Ungültige Beitrags-ID "%s" in {{link:…}} (positive Zahl erwartet, z. B. {{link:42}}).',
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
$type = $this->links?->postTypeOf($id);
|
||||
if ($type === null) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Eintrag %s für {{link:…}} gibt es nicht, er liegt im Papierkorb oder ist nicht lesbar.',
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
return VariableRef::serialize('content', 'post_link_url_' . $type, ['post_id' => $name]);
|
||||
}
|
||||
|
||||
private function field(string $name): string
|
||||
{
|
||||
if (preg_match(DynamicContentRules::META_KEY, $name) !== 1) {
|
||||
|
||||
@@ -26,7 +26,8 @@ defined('ABSPATH') || exit;
|
||||
* settings hue/saturation/lightness/opacity, numbers only.
|
||||
* - type "content": DynamicContentRules – the dynamic content map (ruling S22), custom_meta_<key>
|
||||
* (S12/S29/S30), non-color global variables and theme fonts (S27); it also cleans each content
|
||||
* setting. "enable_html" is always rejected here.
|
||||
* setting. "enable_html" is always rejected here. Internal links post_link_url_<type> ({{link:…}},
|
||||
* E13a) only in URL fields.
|
||||
* A dynamic content map of the wrong shape fails in the constructor with InvalidArgumentException.
|
||||
*/
|
||||
final class VariableRef
|
||||
@@ -232,6 +233,13 @@ final class VariableRef
|
||||
*/
|
||||
private function contentSettings(string $name, array $settings, ValueSanitizer $sanitizer, bool $url): array
|
||||
{
|
||||
if (! $url && str_starts_with($name, 'post_link_url_')) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'"%s" (Link auf einen Eintrag, {{link:…}}) nur in Link- und URL-Feldern.',
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
$fields = $this->content->fieldsFor($name);
|
||||
$clean = [];
|
||||
foreach ($settings as $key => $value) {
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Composer;
|
||||
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
||||
use LIT\MCP\Integrations\Divi\Compose\MediaResolver;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
|
||||
@@ -45,7 +46,8 @@ class DiviComposition
|
||||
private DesignSystemReader $design,
|
||||
private ValueSanitizer $sanitizer = new WpValueSanitizer(),
|
||||
private MediaResolver $media = new WpMediaResolver(),
|
||||
private DynamicContentMap $dynamicContent = new DynamicContentMap()
|
||||
private DynamicContentMap $dynamicContent = new DynamicContentMap(),
|
||||
private LinkTargets $links = new WpLinkTargets()
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ class DiviComposition
|
||||
*/
|
||||
public function placeholders(): Placeholders
|
||||
{
|
||||
return $this->placeholders ??= new Placeholders($this->design->read());
|
||||
return $this->placeholders ??= new Placeholders($this->design->read(), $this->links);
|
||||
}
|
||||
|
||||
/** The one choke point of this composition (ValueCleaner, rulings S1–S3, S7, S13, S28, S32). */
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* {{link:<ID>}} targets from WordPress: the entry exists, lives (no trash, auto-draft or
|
||||
* revision) and the current user may read it (read_post, like select_post settings, S37).
|
||||
* Missing and unreadable entries give the same null (no existence oracle).
|
||||
*/
|
||||
final class WpLinkTargets implements LinkTargets
|
||||
{
|
||||
private const DEAD = ['trash', 'auto-draft', 'inherit'];
|
||||
|
||||
public function postTypeOf(int $id): ?string
|
||||
{
|
||||
$post = get_post($id);
|
||||
if (! $post instanceof WP_Post || in_array((string) $post->post_status, self::DEAD, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return current_user_can('read_post', $id) ? (string) $post->post_type : null;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Support;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Composer;
|
||||
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
||||
use LIT\MCP\Integrations\Divi\Compose\MediaResolver;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
|
||||
@@ -128,9 +129,24 @@ final class ComposeFixture
|
||||
};
|
||||
}
|
||||
|
||||
/** Link targets of the tests: 839 is a page, 840 a post; everything else is missing, trashed or unreadable. */
|
||||
public static function links(): LinkTargets
|
||||
{
|
||||
return new class implements LinkTargets {
|
||||
public function postTypeOf(int $id): ?string
|
||||
{
|
||||
return match ($id) {
|
||||
839 => 'page',
|
||||
840 => 'post',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static function placeholders(): Placeholders
|
||||
{
|
||||
return new Placeholders(DiviFixtures::designSystem());
|
||||
return new Placeholders(DiviFixtures::designSystem(), self::links());
|
||||
}
|
||||
|
||||
/** The strict $variable(…)$ checker for the sample design system and the default map. */
|
||||
|
||||
@@ -6,13 +6,21 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class PlaceholdersTest extends TestCase
|
||||
{
|
||||
/** @return array<string, array{string, string}> */
|
||||
/**
|
||||
* Rows with a third value true go into a URL field (ruling P4: only the link row – URL mode
|
||||
* refuses colour references by design).
|
||||
*
|
||||
* @return array<string, array{0: string, 1: string, 2?: bool}>
|
||||
*/
|
||||
public static function placeholders(): array
|
||||
{
|
||||
return [
|
||||
@@ -31,20 +39,30 @@ final class PlaceholdersTest extends TestCase
|
||||
'{{field:telefon_mobil}}',
|
||||
'$variable({"type":"content","value":{"name":"custom_meta_telefon_mobil","settings":{"before":"","after":""}}})$',
|
||||
],
|
||||
'internal link' => [
|
||||
'{{link:839}}',
|
||||
'$variable({"type":"content","value":{"name":"post_link_url_page","settings":{"post_id":"839"}}})$',
|
||||
true,
|
||||
],
|
||||
'plain text' => ['Hallo Welt', 'Hallo Welt'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider placeholders */
|
||||
public function testResolvesToDiviVariables(string $input, string $expected): void
|
||||
public function testResolvesToDiviVariables(string $input, string $expected, bool $url = false): void
|
||||
{
|
||||
$placeholders = ComposeFixture::placeholders();
|
||||
$resolved = $placeholders->resolve($input);
|
||||
|
||||
self::assertSame($expected, $resolved);
|
||||
if ($expected !== $input) {
|
||||
// What placeholders produce passes the strict checker unchanged (ruling S1/S25).
|
||||
self::assertSame($resolved, ComposeFixture::references()->canonical($resolved, ComposeFixture::sanitizer()));
|
||||
// What placeholders produce passes the strict checker unchanged (ruling S1/S25); the live
|
||||
// registry map knows post_link_url_page, the URL mode allows it.
|
||||
$references = new VariableRef(
|
||||
DiviFixtures::designSystem(),
|
||||
DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry())
|
||||
);
|
||||
self::assertSame($resolved, $references->canonical($resolved, ComposeFixture::sanitizer(), $url));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +78,11 @@ final class PlaceholdersTest extends TestCase
|
||||
'bad field key' => ['{{field:a b}}', 'Ungültiger Feldschlüssel'],
|
||||
'protected field key' => ['{{field:_edit_lock}}', 'Ungültiger Feldschlüssel'],
|
||||
'embedded' => ['Seite: {{post:title}}', 'allein im Wert'],
|
||||
'link without id' => ['{{link:start}}', 'Ungültige Beitrags-ID'],
|
||||
'link to a missing post' => ['{{link:999}}', 'Eintrag 999'],
|
||||
'link without link targets' => ['{{link:0}}', 'Ungültige Beitrags-ID'],
|
||||
'link with a leading zero' => ['{{link:0839}}', 'Ungültige Beitrags-ID'],
|
||||
'link beyond PHP_INT_MAX' => ['{{link:9999999999999999999}}', 'Ungültige Beitrags-ID'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -137,4 +160,12 @@ final class PlaceholdersTest extends TestCase
|
||||
ComposeFixture::placeholders()->resolveDeep(['color' => '{{color:primary}}', 'size' => '20px'])
|
||||
);
|
||||
}
|
||||
|
||||
public function testLinksNeedLinkTargets(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Eintrag 839');
|
||||
|
||||
(new Placeholders(DiviFixtures::designSystem()))->resolve('{{link:839}}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ValueCleaner;
|
||||
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -352,5 +357,73 @@ final class ShortformWriterTest extends TestCase
|
||||
self::assertStringContainsString('gcid-a1b2', $color['attrs']['divider']['advanced']['line']['desktop']['value']['color']);
|
||||
}
|
||||
}
|
||||
|
||||
/** E13a: {{link:ID}} becomes Divi's post_link_url_<type> in URL fields – and only there. */
|
||||
public function testInternalLinkOnlyInUrlFields(): void
|
||||
{
|
||||
$writer = self::linkWriter();
|
||||
|
||||
$button = $writer->apply('button', [], ['text' => 'Kontakt', 'url' => '{{link:839}}'], 'n');
|
||||
self::assertSame([], $button['errors']);
|
||||
self::assertSame(
|
||||
'$variable({"type":"content","value":{"name":"post_link_url_page","settings":{"post_id":"839"}}})$',
|
||||
$button['attrs']['button']['innerContent']['desktop']['value']['linkUrl']
|
||||
);
|
||||
|
||||
$text = $writer->apply('button', [], ['text' => '{{link:839}}'], 'n');
|
||||
self::assertStringContainsString('nur in Link- und URL-Feldern', $text['errors']['n.text']);
|
||||
|
||||
// A post type the registry offers no link option for (fixture: page only): unknown name.
|
||||
$post = $writer->apply('button', [], ['url' => '{{link:840}}'], 'n');
|
||||
self::assertStringContainsString('post_link_url_post', $post['errors']['n.url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* The full form goes through the same choke point: URL fields only, no before/after, and the
|
||||
* target must be readable (select_post, ruling S37 / preflight §4) – no path around ValueCleaner.
|
||||
*/
|
||||
public function testInternalLinkFullFormIsCheckedLikeThePlaceholder(): void
|
||||
{
|
||||
$writer = self::linkWriter();
|
||||
$link = static fn (string $settings): string =>
|
||||
'$variable({"type":"content","value":{"name":"post_link_url_page","settings":' . $settings . '}})$';
|
||||
|
||||
$valid = $writer->apply('button', [], ['url' => $link('{"post_id":839}')], 'n');
|
||||
self::assertSame([], $valid['errors']);
|
||||
self::assertSame($link('{"post_id":839}'), $valid['attrs']['button']['innerContent']['desktop']['value']['linkUrl']);
|
||||
|
||||
$unreadable = $writer->apply('button', [], ['url' => $link('{"post_id":"666"}')], 'n');
|
||||
self::assertStringContainsString('lesbaren Eintrags', $unreadable['errors']['n.url']);
|
||||
|
||||
$slug = $writer->apply('button', [], ['url' => $link('{"post_id":"kontakt"}')], 'n');
|
||||
self::assertStringContainsString('lesbaren Eintrags', $slug['errors']['n.url']);
|
||||
|
||||
$extra = $writer->apply('button', [], ['url' => $link('{"post_id":"839","before":"<b>x</b>"}')], 'n');
|
||||
self::assertArrayHasKey('n.url', $extra['errors']);
|
||||
self::assertSame([], $extra['attrs']);
|
||||
|
||||
$inText = $writer->apply('heading', [], ['text' => $link('{"post_id":"839"}')], 'n');
|
||||
self::assertStringContainsString('nur in Link- und URL-Feldern', $inText['errors']['n.text']);
|
||||
|
||||
// A control-split placeholder is the placeholder (S35) and resolves the same way.
|
||||
$split = $writer->apply('button', [], ['url' => '{' . chr(0) . '{link:839}}'], 'n');
|
||||
self::assertSame([], $split['errors']);
|
||||
self::assertSame(
|
||||
$link('{"post_id":"839"}'),
|
||||
$split['attrs']['button']['innerContent']['desktop']['value']['linkUrl']
|
||||
);
|
||||
}
|
||||
|
||||
/** ShortformWriter with the live registry map (knows post_link_url_page) and the test link targets. */
|
||||
private static function linkWriter(): ShortformWriter
|
||||
{
|
||||
$design = DiviFixtures::designSystem();
|
||||
$cleaner = new ValueCleaner(
|
||||
DiviFixtures::catalog(),
|
||||
new VariableRef($design, DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry())),
|
||||
ComposeFixture::sanitizer()
|
||||
);
|
||||
|
||||
return new ShortformWriter(new Placeholders($design, ComposeFixture::links()), ComposeFixture::media(), $cleaner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,11 +421,11 @@ final class VariableRefTest extends TestCase
|
||||
);
|
||||
self::assertSame(
|
||||
self::content('post_link_url_page', '{"post_id":"12"}'),
|
||||
$ref->canonical(self::content('post_link_url_page', '{"post_id":"12"}'), $sanitizer)
|
||||
$ref->canonical(self::content('post_link_url_page', '{"post_id":"12"}'), $sanitizer, true)
|
||||
);
|
||||
self::assertSame(
|
||||
self::content('post_link_url_page', '{"post_id":12}'),
|
||||
$ref->canonical(self::content('post_link_url_page', '{"post_id":12}'), $sanitizer)
|
||||
$ref->canonical(self::content('post_link_url_page', '{"post_id":12}'), $sanitizer, true)
|
||||
);
|
||||
|
||||
$rejected = [
|
||||
@@ -440,12 +440,25 @@ final class VariableRefTest extends TestCase
|
||||
];
|
||||
foreach ($rejected as $case => [$value, $fragment]) {
|
||||
try {
|
||||
$ref->canonical($value, $sanitizer);
|
||||
// URL mode: internal links (post_link_url_*) are allowed only there (E13a).
|
||||
$ref->canonical($value, $sanitizer, true);
|
||||
self::fail('Accepted: ' . $case);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
self::assertStringContainsString($fragment, $e->getMessage(), $case);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** E13a: an internal link (post_link_url_<type>) is a URL – refused in every other field. */
|
||||
public function testInternalLinksOnlyInUrlFields(): void
|
||||
{
|
||||
$ref = self::ref(['post_link_url_page' => ['post_id' => 'select_post']]);
|
||||
$link = self::content('post_link_url_page', '{"post_id":"12"}');
|
||||
|
||||
self::assertSame($link, $ref->canonical($link, ComposeFixture::sanitizer(), true));
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('nur in Link- und URL-Feldern');
|
||||
$ref->canonical($link, ComposeFixture::sanitizer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,14 @@ final class DiviCompositionTest extends TestCase
|
||||
return DiviFixtures::dynamicContentRegistry();
|
||||
});
|
||||
|
||||
return new DiviComposition($modules, $design, ComposeFixture::sanitizer(), ComposeFixture::media(), $registry);
|
||||
return new DiviComposition(
|
||||
$modules,
|
||||
$design,
|
||||
ComposeFixture::sanitizer(),
|
||||
ComposeFixture::media(),
|
||||
$registry,
|
||||
ComposeFixture::links()
|
||||
);
|
||||
}
|
||||
|
||||
private static function siteTitle(): string
|
||||
@@ -74,6 +81,24 @@ final class DiviCompositionTest extends TestCase
|
||||
self::assertSame($composition->references(), $composition->references());
|
||||
}
|
||||
|
||||
/** E13a: the composition hands its link targets to Placeholders; the result still passes ValueCleaner. */
|
||||
public function testInternalLinksUseTheLinkTargets(): void
|
||||
{
|
||||
$composer = $this->composition()->forPost(7)->composer();
|
||||
|
||||
$column = Block::create('divi/column');
|
||||
|
||||
$page = $composer->compose([['type' => 'button', 'text' => 'Kontakt', 'url' => '{{link:839}}']], $column, 'nodes');
|
||||
self::assertSame([], $page['errors']);
|
||||
self::assertStringContainsString(
|
||||
'post_link_url_page',
|
||||
json_encode($page['blocks'][0]->attrs(), JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$missing = $composer->compose([['type' => 'button', 'url' => '{{link:999}}']], $column, 'nodes');
|
||||
self::assertStringContainsString('Eintrag 999', $missing['errors']['nodes[0].url']);
|
||||
}
|
||||
|
||||
public function testWithoutAPostTheDefaultMapApplies(): void
|
||||
{
|
||||
$composition = $this->composition();
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Pages\WpLinkTargets;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use WP_Post;
|
||||
|
||||
final class WpLinkTargetsTest extends TestCase
|
||||
{
|
||||
public function testOnlyReadableLivingPostsCanBeLinked(): void
|
||||
{
|
||||
$posts = [
|
||||
839 => new WP_Post(['ID' => 839, 'post_type' => 'page', 'post_status' => 'draft']),
|
||||
841 => new WP_Post(['ID' => 841, 'post_type' => 'page', 'post_status' => 'trash']),
|
||||
842 => new WP_Post(['ID' => 842, 'post_type' => 'page', 'post_status' => 'private']),
|
||||
843 => new WP_Post(['ID' => 843, 'post_type' => 'revision', 'post_status' => 'inherit']),
|
||||
844 => new WP_Post(['ID' => 844, 'post_type' => 'page', 'post_status' => 'auto-draft']),
|
||||
];
|
||||
Functions\when('get_post')->alias(static fn (int $id): ?WP_Post => $posts[$id] ?? null);
|
||||
Functions\when('current_user_can')->alias(
|
||||
static fn (string $cap, int $id): bool => $cap === 'read_post' && $id !== 842
|
||||
);
|
||||
|
||||
$links = new WpLinkTargets();
|
||||
|
||||
self::assertSame('page', $links->postTypeOf(839));
|
||||
self::assertNull($links->postTypeOf(841), 'trashed');
|
||||
self::assertNull($links->postTypeOf(842), 'not readable');
|
||||
self::assertNull($links->postTypeOf(843), 'revision');
|
||||
self::assertNull($links->postTypeOf(844), 'auto-draft');
|
||||
self::assertNull($links->postTypeOf(900), 'missing');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user