feat: lit/page-render-summary – rendered content as structured excerpt
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -19,8 +19,8 @@ des verbundenen WordPress-Benutzers.
|
|||||||
|
|
||||||
Direkt sichtbar: `lit-guide`, `lit-site-status`, `lit-content-types`, `lit-content-query`,
|
Direkt sichtbar: `lit-guide`, `lit-site-status`, `lit-content-types`, `lit-content-query`,
|
||||||
`lit-content-get`, `lit-content-create`, `lit-content-update`, `lit-post-publish`, `lit-media-search`,
|
`lit-content-get`, `lit-content-create`, `lit-content-update`, `lit-post-publish`, `lit-media-search`,
|
||||||
`lit-media-preview`, `lit-media-download`, `lit-media-upload-from-url`, `lit-divi-outline`,
|
`lit-media-preview`, `lit-media-download`, `lit-media-upload-from-url`, `lit-page-render-summary`,
|
||||||
`lit-divi-node-get`, `lit-divi-catalog`, `lit-divi-design-system`.
|
`lit-divi-outline`, `lit-divi-node-get`, `lit-divi-catalog`, `lit-divi-design-system`.
|
||||||
|
|
||||||
Welche Tools direkt sichtbar sind, lässt sich unter Einstellungen → LIT MCP → Tools ändern (außer bei
|
Welche Tools direkt sichtbar sind, lässt sich unter Einstellungen → LIT MCP → Tools ändern (außer bei
|
||||||
`lit-guide`, das immer direkt bleibt).
|
`lit-guide`, das immer direkt bleibt).
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\WordPress\Render;
|
||||||
|
|
||||||
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lit/page-render-summary: what a visitor sees in the content area – headings, texts, links,
|
||||||
|
* images with alt text and obvious problems (spec §5.7, §8.1). Works for every managed entry,
|
||||||
|
* Divi or not; the content is rendered in-process, header/footer/Theme Builder are not included.
|
||||||
|
*/
|
||||||
|
final class PageRenderSummaryAbility implements Ability
|
||||||
|
{
|
||||||
|
public function __construct(private ContentSchema $schema, private PageRenderer $renderer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'lit/page-render-summary';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'Gerenderte Seite (Auszug)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Rendert den Inhaltsbereich eines Eintrags wie im Frontend und liefert einen strukturierten Auszug: '
|
||||||
|
. 'Überschriften, Texte, Links, Bilder mit Alt-Text und Warnungen (Hierarchie-Sprünge, fehlende '
|
||||||
|
. 'Alt-Texte). Header, Footer und Theme-Builder-Vorlagen sind nicht enthalten. Gut zum Prüfen nach '
|
||||||
|
. 'Änderungen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => ['post_id' => ['type' => 'integer', 'minimum' => 1]],
|
||||||
|
'required' => ['post_id'],
|
||||||
|
'additionalProperties' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function outputSchema(): ?array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'post_id' => ['type' => 'integer'],
|
||||||
|
'url' => ['type' => 'string'],
|
||||||
|
'headings' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||||||
|
'texts' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
|
'links' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||||||
|
'images' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||||||
|
'warnings' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
|
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
|
],
|
||||||
|
'required' => ['post_id', 'headings', 'texts', 'links', 'images', 'warnings', 'notes'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function annotations(): array
|
||||||
|
{
|
||||||
|
return ['readonly' => true, 'destructive' => false, 'idempotent' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDirect(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permission(array $input): bool|WP_Error
|
||||||
|
{
|
||||||
|
$post = PostGuard::editable($input['post_id'] ?? null, $this->schema, 'post_id', true);
|
||||||
|
|
||||||
|
return $post instanceof WP_Error ? $post : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(array $input): mixed
|
||||||
|
{
|
||||||
|
$post = PostGuard::editable($input['post_id'] ?? null, $this->schema, 'post_id', true);
|
||||||
|
if ($post instanceof WP_Error) {
|
||||||
|
return $post;
|
||||||
|
}
|
||||||
|
|
||||||
|
$notes = ['Nur der Inhaltsbereich; Header, Footer und Theme-Builder-Vorlagen fehlen.'];
|
||||||
|
if ((string) $post->post_password !== '') {
|
||||||
|
$notes[] = 'Der Eintrag ist passwortgeschützt; Besucher sehen zuerst das Passwortformular.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_merge(
|
||||||
|
['post_id' => (int) $post->ID, 'url' => (string) get_permalink($post)],
|
||||||
|
RenderSummary::fromHtml($this->renderer->render($post)),
|
||||||
|
['notes' => $notes]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\WordPress\Render;
|
||||||
|
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the content area of an entry in-process, like the front end does for the_content
|
||||||
|
* (Divi 5 renders its blocks there – verified in DDEV). Header, footer and Theme Builder
|
||||||
|
* layouts are not part of it. Not final so abilities can be unit-tested with a mock.
|
||||||
|
*/
|
||||||
|
class PageRenderer
|
||||||
|
{
|
||||||
|
public function render(WP_Post $post): string
|
||||||
|
{
|
||||||
|
$previous = $GLOBALS['post'] ?? null;
|
||||||
|
$GLOBALS['post'] = $post;
|
||||||
|
setup_postdata($post);
|
||||||
|
ob_start();
|
||||||
|
try {
|
||||||
|
$html = (string) apply_filters('the_content', (string) $post->post_content);
|
||||||
|
} finally {
|
||||||
|
$echoed = (string) ob_get_clean();
|
||||||
|
$GLOBALS['post'] = $previous;
|
||||||
|
if ($previous instanceof WP_Post) {
|
||||||
|
setup_postdata($previous);
|
||||||
|
} else {
|
||||||
|
wp_reset_postdata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $echoed . $html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\WordPress\Render;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use DOMElement;
|
||||||
|
use DOMXPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structured excerpt of rendered HTML (spec §5.7 lit/page-render-summary): headings, text
|
||||||
|
* blocks, links, images with alt text and the obvious problems. Pure PHP (ext-dom).
|
||||||
|
*/
|
||||||
|
final class RenderSummary
|
||||||
|
{
|
||||||
|
private const TEXT_LIMIT = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{headings: list<array{level: int, text: string}>, texts: list<string>,
|
||||||
|
* links: list<array{text: string, href: string}>, images: list<array{src: string, alt: ?string}>,
|
||||||
|
* warnings: list<string>}
|
||||||
|
*/
|
||||||
|
public static function fromHtml(string $html, int $max = 50): array
|
||||||
|
{
|
||||||
|
$dom = new DOMDocument();
|
||||||
|
$previous = libxml_use_internal_errors(true);
|
||||||
|
$dom->loadHTML(
|
||||||
|
'<?xml encoding="UTF-8"><html><body>' . $html . '</body></html>',
|
||||||
|
LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING
|
||||||
|
);
|
||||||
|
libxml_clear_errors();
|
||||||
|
libxml_use_internal_errors($previous);
|
||||||
|
$xpath = new DOMXPath($dom);
|
||||||
|
|
||||||
|
$headings = [];
|
||||||
|
foreach (self::elements($xpath, '//h1|//h2|//h3|//h4|//h5|//h6', $max) as $element) {
|
||||||
|
$headings[] = ['level' => (int) substr($element->nodeName, 1), 'text' => self::text($element)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$texts = [];
|
||||||
|
foreach (self::elements($xpath, '//p|//li|//blockquote', $max) as $element) {
|
||||||
|
$text = self::text($element);
|
||||||
|
if ($text !== '') {
|
||||||
|
$texts[] = mb_strlen($text) > self::TEXT_LIMIT
|
||||||
|
? mb_substr($text, 0, self::TEXT_LIMIT - 1) . '…'
|
||||||
|
: $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$links = [];
|
||||||
|
foreach (self::elements($xpath, '//a[@href]', $max) as $element) {
|
||||||
|
$links[] = ['text' => self::text($element), 'href' => $element->getAttribute('href')];
|
||||||
|
}
|
||||||
|
|
||||||
|
$images = [];
|
||||||
|
foreach (self::elements($xpath, '//img', $max) as $element) {
|
||||||
|
$images[] = [
|
||||||
|
'src' => $element->getAttribute('src'),
|
||||||
|
'alt' => $element->hasAttribute('alt') ? $element->getAttribute('alt') : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'headings' => $headings,
|
||||||
|
'texts' => $texts,
|
||||||
|
'links' => $links,
|
||||||
|
'images' => $images,
|
||||||
|
'warnings' => self::warnings($headings, $images, $links),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{level: int, text: string}> $headings
|
||||||
|
* @param list<array{src: string, alt: ?string}> $images
|
||||||
|
* @param list<array{text: string, href: string}> $links
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private static function warnings(array $headings, array $images, array $links): array
|
||||||
|
{
|
||||||
|
$warnings = [];
|
||||||
|
$previous = null;
|
||||||
|
foreach ($headings as $heading) {
|
||||||
|
if ($previous !== null && $heading['level'] > $previous + 1) {
|
||||||
|
$warnings[] = sprintf(
|
||||||
|
'Sprung in der Überschriftenhierarchie: h%d → h%d ("%s").',
|
||||||
|
$previous,
|
||||||
|
$heading['level'],
|
||||||
|
$heading['text']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$previous = $heading['level'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$h1 = count(array_filter($headings, static fn (array $heading): bool => $heading['level'] === 1));
|
||||||
|
if ($h1 > 1) {
|
||||||
|
$warnings[] = sprintf('%d Überschriften der Ebene h1 im Inhalt.', $h1);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($images as $image) {
|
||||||
|
if ($image['alt'] === null || trim($image['alt']) === '') {
|
||||||
|
$warnings[] = 'Bild ohne Alt-Text: ' . $image['src'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($links as $link) {
|
||||||
|
if ($link['href'] === '' || $link['href'] === '#') {
|
||||||
|
$warnings[] = sprintf('Link ohne Ziel: "%s".', $link['text']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<DOMElement> */
|
||||||
|
private static function elements(DOMXPath $xpath, string $query, int $max): array
|
||||||
|
{
|
||||||
|
$nodes = $xpath->query($query);
|
||||||
|
$elements = [];
|
||||||
|
foreach ($nodes === false ? [] : $nodes as $node) {
|
||||||
|
if ($node instanceof DOMElement && count($elements) < $max) {
|
||||||
|
$elements[] = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function text(DOMElement $element): string
|
||||||
|
{
|
||||||
|
return trim((string) preg_replace('/\s+/u', ' ', $element->textContent));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ use LIT\MCP\Integrations\WordPress\Menus\MenuListAbility;
|
|||||||
use LIT\MCP\Integrations\WordPress\Menus\MenuOperations;
|
use LIT\MCP\Integrations\WordPress\Menus\MenuOperations;
|
||||||
use LIT\MCP\Integrations\WordPress\Menus\MenuSetItemsAbility;
|
use LIT\MCP\Integrations\WordPress\Menus\MenuSetItemsAbility;
|
||||||
use LIT\MCP\Integrations\WordPress\Menus\MenuWriter;
|
use LIT\MCP\Integrations\WordPress\Menus\MenuWriter;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Render\PageRenderSummaryAbility;
|
||||||
use LIT\MCP\Integrations\WordPress\Revisions\RevisionRestoreAbility;
|
use LIT\MCP\Integrations\WordPress\Revisions\RevisionRestoreAbility;
|
||||||
use LIT\MCP\Integrations\WordPress\Revisions\RevisionsListAbility;
|
use LIT\MCP\Integrations\WordPress\Revisions\RevisionsListAbility;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
@@ -37,7 +39,8 @@ use LIT\MCP\Integrations\WordPress\Status\PostTrashAbility;
|
|||||||
use LIT\MCP\Integrations\WordPress\Status\PostUnpublishAbility;
|
use LIT\MCP\Integrations\WordPress\Status\PostUnpublishAbility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WordPress basics (spec §6): content, status, copies, revisions, menus, settings.
|
* WordPress basics (spec §6): content, status, copies, revisions, menus, settings, and the
|
||||||
|
* rendered-content summary lit/page-render-summary (spec §5.7 – useful for every entry).
|
||||||
* Always available.
|
* Always available.
|
||||||
*/
|
*/
|
||||||
final class WordPressIntegration implements Integration
|
final class WordPressIntegration implements Integration
|
||||||
@@ -84,6 +87,7 @@ final class WordPressIntegration implements Integration
|
|||||||
new MenuAssignLocationAbility($this->log),
|
new MenuAssignLocationAbility($this->log),
|
||||||
new WpSettingsGetAbility($whitelist),
|
new WpSettingsGetAbility($whitelist),
|
||||||
new WpSettingsUpdateAbility($whitelist, $this->log),
|
new WpSettingsUpdateAbility($whitelist, $this->log),
|
||||||
|
new PageRenderSummaryAbility($this->schema, new PageRenderer()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Render;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Render\PageRenderSummaryAbility;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Render\RenderSummary;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
final class PageRenderSummaryAbilityTest extends TestCase
|
||||||
|
{
|
||||||
|
private const HTML = '<div class="et_pb_section"><h1>Willkommen</h1><h3>Leistungen</h3>'
|
||||||
|
. '<p>Seit 1987 <a href="/kontakt">in Köln</a>.</p><ul><li>Beratung</li></ul>'
|
||||||
|
. '<img src="https://k.test/a.jpg" alt="Team"><img src="https://k.test/b.jpg"><a href="#">Mehr</a></div>';
|
||||||
|
|
||||||
|
public function testSummarizesHtml(): void
|
||||||
|
{
|
||||||
|
$summary = RenderSummary::fromHtml(self::HTML);
|
||||||
|
|
||||||
|
self::assertSame([['level' => 1, 'text' => 'Willkommen'], ['level' => 3, 'text' => 'Leistungen']], $summary['headings']);
|
||||||
|
self::assertSame(['Seit 1987 in Köln.', 'Beratung'], $summary['texts']);
|
||||||
|
self::assertSame(['text' => 'in Köln', 'href' => '/kontakt'], $summary['links'][0]);
|
||||||
|
self::assertSame(
|
||||||
|
[['src' => 'https://k.test/a.jpg', 'alt' => 'Team'], ['src' => 'https://k.test/b.jpg', 'alt' => null]],
|
||||||
|
$summary['images']
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Sprung in der Überschriftenhierarchie: h1 → h3 ("Leistungen").',
|
||||||
|
'Bild ohne Alt-Text: https://k.test/b.jpg',
|
||||||
|
'Link ohne Ziel: "Mehr".',
|
||||||
|
],
|
||||||
|
$summary['warnings']
|
||||||
|
);
|
||||||
|
self::assertSame([], RenderSummary::fromHtml('')['headings']);
|
||||||
|
self::assertCount(2, RenderSummary::fromHtml(str_repeat('<p>x</p>', 5), 2)['texts']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRendersTheEntryAndAddsNotes(): void
|
||||||
|
{
|
||||||
|
$post = new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_password' => 'geheim']);
|
||||||
|
Functions\when('get_post')->justReturn($post);
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
Functions\when('get_permalink')->justReturn('https://k.test/start/');
|
||||||
|
$schema = Mockery::mock(ContentSchema::class);
|
||||||
|
$schema->allows('isManaged')->andReturn(true);
|
||||||
|
$renderer = Mockery::mock(PageRenderer::class);
|
||||||
|
$renderer->expects('render')->with($post)->andReturn(self::HTML);
|
||||||
|
$ability = new PageRenderSummaryAbility($schema, $renderer);
|
||||||
|
|
||||||
|
self::assertTrue($ability->permission(['post_id' => 7]));
|
||||||
|
$result = $ability->execute(['post_id' => 7]);
|
||||||
|
|
||||||
|
self::assertSame(7, $result['post_id']);
|
||||||
|
self::assertSame('https://k.test/start/', $result['url']);
|
||||||
|
self::assertCount(2, $result['headings']);
|
||||||
|
self::assertCount(2, $result['notes']);
|
||||||
|
self::assertTrue($ability->isDirect());
|
||||||
|
self::assertTrue($ability->annotations()['readonly']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Render;
|
||||||
|
|
||||||
|
use Brain\Monkey\Filters;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use RuntimeException;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
final class PageRendererTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
unset($GLOBALS['post']);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRendersTheContentFilterAndRestoresTheGlobalPost(): void
|
||||||
|
{
|
||||||
|
$previous = new WP_Post(['ID' => 1]);
|
||||||
|
$GLOBALS['post'] = $previous;
|
||||||
|
$post = new WP_Post(['ID' => 7, 'post_content' => '<!-- wp:divi/section /-->']);
|
||||||
|
$setup = [];
|
||||||
|
Functions\when('setup_postdata')->alias(static function (WP_Post $p) use (&$setup): bool {
|
||||||
|
$setup[] = $p->ID;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
Filters\expectApplied('the_content')->once()->with('<!-- wp:divi/section /-->')->andReturnUsing(
|
||||||
|
static function () use ($post): string {
|
||||||
|
echo '<p>echoed</p>';
|
||||||
|
|
||||||
|
return $GLOBALS['post'] === $post ? '<div class="et_pb_section"></div>' : 'wrong post';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$html = (new PageRenderer())->render($post);
|
||||||
|
|
||||||
|
self::assertSame('<p>echoed</p><div class="et_pb_section"></div>', $html);
|
||||||
|
self::assertSame($previous, $GLOBALS['post']);
|
||||||
|
self::assertSame([7, 1], $setup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRestoresEvenWhenRenderingThrows(): void
|
||||||
|
{
|
||||||
|
Functions\when('setup_postdata')->justReturn(true);
|
||||||
|
Functions\expect('wp_reset_postdata')->once();
|
||||||
|
Filters\expectApplied('the_content')->once()->andReturnUsing(static function (): string {
|
||||||
|
throw new RuntimeException('module crashed');
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
(new PageRenderer())->render(new WP_Post(['ID' => 7]));
|
||||||
|
self::fail('exception expected');
|
||||||
|
} catch (RuntimeException) {
|
||||||
|
self::assertArrayHasKey('post', $GLOBALS);
|
||||||
|
self::assertNull($GLOBALS['post']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ final class WordPressIntegrationTest extends TestCase
|
|||||||
'lit/menu-assign-location',
|
'lit/menu-assign-location',
|
||||||
'lit/wp-settings-get',
|
'lit/wp-settings-get',
|
||||||
'lit/wp-settings-update',
|
'lit/wp-settings-update',
|
||||||
|
'lit/page-render-summary',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function testProvidesTheWordPressAbilitiesInOrder(): void
|
public function testProvidesTheWordPressAbilitiesInOrder(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user