feat: lit-site-status, Admin-Tab und Admin-Hinweis zeigen Divi-Fähigkeiten mit Zustand, Quelle, Problemen und betroffenen Werkzeugen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
b92545a8fe
commit
2aef6e1154
+142
-10
@@ -10,6 +10,8 @@ use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
@@ -21,10 +23,12 @@ use LIT\MCP\Plugin;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Tab "Divi" (spec §6): release status of the installed Divi, the latest check report, "Prüfung starten" (REST,
|
||||
* DiviCheckRoute), "Divi <Version> freigeben" (only after a fresh green report) and "Freigabe zurückziehen" (only
|
||||
* for a database release). Loading the tab removes check pages a dead check left behind – in the page's load hook
|
||||
* (removeOrphans()), not while rendering.
|
||||
* Tab "Divi" (spec §6): release status of the installed Divi, the table of its features (state, source, problems,
|
||||
* affected tools – E3), the latest check report, "Prüfung starten" (REST, DiviCheckRoute), "Divi <Version>
|
||||
* freigeben" (only after a fresh green report) and "Freigabe zurückziehen" (whenever a database release applies,
|
||||
* whatever the overall state). Loading the tab removes check pages a dead check left behind – in the page's load
|
||||
* hook (removeOrphans()), not while rendering. renderNotice() is the admin notice on every admin page while a
|
||||
* feature is unusable.
|
||||
*/
|
||||
final class DiviTab implements AdminTab
|
||||
{
|
||||
@@ -38,6 +42,7 @@ final class DiviTab implements AdminTab
|
||||
/**
|
||||
* @param (Closure(): string)|null $version Installed Divi version ('' = none); null = DiviEnvironment.
|
||||
* @param (Closure(string): string)|null $stateKey null = ReleaseResolver::stateKey().
|
||||
* @param FeatureOverview|null $overview null = a table without the affected tools.
|
||||
*/
|
||||
public function __construct(
|
||||
private ?CheckReportStore $reports = null,
|
||||
@@ -45,7 +50,8 @@ final class DiviTab implements AdminTab
|
||||
private ?ChangeLog $log = null,
|
||||
private ?AdminPostAction $action = null,
|
||||
private ?Closure $version = null,
|
||||
private ?Closure $stateKey = null
|
||||
private ?Closure $stateKey = null,
|
||||
private ?FeatureOverview $overview = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -68,6 +74,55 @@ final class DiviTab implements AdminTab
|
||||
add_action('admin_post_' . self::RELEASE_ACTION, [$this, 'release']);
|
||||
add_action('admin_post_' . self::WITHDRAW_ACTION, [$this, 'withdraw']);
|
||||
add_action('load-' . SettingsPage::SCREEN_HOOK, [$this, 'removeOrphans']);
|
||||
$this->registerNotice();
|
||||
}
|
||||
|
||||
/** The admin notice for administrators while a Divi feature is unusable (registerActions() runs in admin only). */
|
||||
public function registerNotice(): void
|
||||
{
|
||||
add_action('admin_notices', [$this, 'renderNotice']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for administrators, only with Divi active, only while a feature is not usable – and not on the Divi tab,
|
||||
* which shows the same table. Reads the cached matrix (D7), so no check runs on an ordinary admin page.
|
||||
*/
|
||||
public function renderNotice(): void
|
||||
{
|
||||
if (! current_user_can('manage_options') || $this->version() === '' || $this->onDiviTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unusable = ReleaseGate::status()->matrix->unusable();
|
||||
if ($unusable === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>',
|
||||
esc_html(sprintf(
|
||||
/* translators: 1: Divi version, 2: comma separated feature names */
|
||||
__(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||
'LIT MCP: Mit Divi %1$s sind diese Fähigkeiten nicht nutzbar: %2$s. Die betroffenen Werkzeuge lehnen ab, alle anderen laufen weiter.',
|
||||
'wp-lit-mcp'
|
||||
),
|
||||
$this->version(),
|
||||
implode(', ', array_map(
|
||||
static fn (FeatureStatus $status): string => $status->feature->label(),
|
||||
$unusable
|
||||
))
|
||||
)),
|
||||
esc_url(SettingsPage::url(self::SLUG)),
|
||||
esc_html__('Details im Reiter „Divi“', 'wp-lit-mcp')
|
||||
);
|
||||
}
|
||||
|
||||
private function onDiviTab(): bool
|
||||
{
|
||||
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
||||
|
||||
return is_object($screen) && $screen->id === SettingsPage::SCREEN_HOOK && $this->requested();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +208,7 @@ final class DiviTab implements AdminTab
|
||||
echo '<h2>' . esc_html__('Divi-Freigabe', 'wp-lit-mcp') . '</h2>';
|
||||
echo '<p>' . esc_html__(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||
'LIT MCP schreibt nur mit Divi-Versionen, die geprüft sind. Nach einem Divi-Update hier die Prüfung starten und die Version freigeben; bis dahin lesen die Divi-Werkzeuge nur.',
|
||||
'LIT MCP prüft nach einem Divi-Update jede Fähigkeit einzeln (z. B. „Seiten schreiben“). Ist sie nicht nutzbar, lehnen nur die Werkzeuge ab, die sie brauchen; alle anderen laufen weiter.',
|
||||
'wp-lit-mcp'
|
||||
) . '</p>';
|
||||
if ($this->removed !== []) {
|
||||
@@ -168,6 +223,7 @@ final class DiviTab implements AdminTab
|
||||
}
|
||||
|
||||
$this->renderStatus($status);
|
||||
$this->renderFeatures($status);
|
||||
$this->renderForms($status, $report);
|
||||
if ($report !== null) {
|
||||
$this->renderReport($report);
|
||||
@@ -184,7 +240,7 @@ final class DiviTab implements AdminTab
|
||||
);
|
||||
printf(
|
||||
'<tr><th scope="row">%s</th><td><strong>%s</strong> – %s</td></tr>',
|
||||
esc_html__('Zustand', 'wp-lit-mcp'),
|
||||
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
|
||||
esc_html($this->stateLabel($status->state)),
|
||||
esc_html($this->explanation($status))
|
||||
);
|
||||
@@ -198,6 +254,80 @@ final class DiviTab implements AdminTab
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
private function renderFeatures(ReleaseStatus $status): void
|
||||
{
|
||||
echo '<h3>' . esc_html__('Fähigkeiten', 'wp-lit-mcp') . '</h3>';
|
||||
echo '<table class="widefat striped lit-mcp-divi-features"><thead><tr>';
|
||||
$headings = [
|
||||
__('Fähigkeit', 'wp-lit-mcp'),
|
||||
__('Zustand', 'wp-lit-mcp'),
|
||||
__('Quelle', 'wp-lit-mcp'),
|
||||
__('Probleme', 'wp-lit-mcp'),
|
||||
__('Betroffene Werkzeuge', 'wp-lit-mcp'),
|
||||
];
|
||||
foreach ($headings as $heading) {
|
||||
echo '<th scope="col">' . esc_html($heading) . '</th>';
|
||||
}
|
||||
echo '</tr></thead><tbody>';
|
||||
foreach (($this->overview ?? FeatureOverview::withoutTools())->rows($status->matrix) as $row) {
|
||||
echo '<tr><td>' . esc_html($row['label']) . '</td>';
|
||||
echo '<td><strong>' . esc_html($this->stateLabel(ReleaseState::from($row['state']))) . '</strong></td>';
|
||||
echo '<td>' . esc_html($this->sourceLabel($row['source'])) . '</td>';
|
||||
echo '<td>';
|
||||
$this->renderList($row['problems'], false);
|
||||
echo '</td><td>';
|
||||
$this->renderList($row['tools'], true);
|
||||
echo '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
$this->renderAutomationNote();
|
||||
}
|
||||
|
||||
/** @param list<string> $items */
|
||||
private function renderList(array $items, bool $code): void
|
||||
{
|
||||
if ($items === []) {
|
||||
echo '–';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<ul>';
|
||||
foreach ($items as $item) {
|
||||
echo $code ? '<li><code>' . esc_html($item) . '</code></li>' : '<li>' . esc_html($item) . '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
|
||||
private function sourceLabel(?string $source): string
|
||||
{
|
||||
return match ($source) {
|
||||
ReleaseStatus::SOURCE_CODE => __('Plugin (code)', 'wp-lit-mcp'),
|
||||
ReleaseStatus::SOURCE_DATABASE => __('Freigabe auf dieser Website (database)', 'wp-lit-mcp'),
|
||||
ReleaseStatus::SOURCE_AUTO => __('Automatik (auto)', 'wp-lit-mcp'),
|
||||
default => '–',
|
||||
};
|
||||
}
|
||||
|
||||
/** E1: what the automatic release does and how to switch it off. */
|
||||
private function renderAutomationNote(): void
|
||||
{
|
||||
echo '<p class="description">' . esc_html__(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||
'Automatik: Ist die installierte Divi-Version im Plugin nicht als getestet hinterlegt, gibt LIT MCP jede Fähigkeit einzeln frei, wenn alle benutzten Divi-Funktionen vorhanden sind, ihre Aufrufe (Signaturen) und die Fingerabdrücke nachgebauter Divi-Abläufe unverändert sind und alle Konstanten definiert sind. Das prüft Aufrufformen, nicht das Verhalten von Divi. Fehlt für eine Stelle die Referenz, ist die Fähigkeit ungetestet; weicht etwas ab, ist sie inkompatibel.',
|
||||
'wp-lit-mcp'
|
||||
) . '</p>';
|
||||
echo '<p class="description">' . sprintf(
|
||||
/* translators: %s: filter name in a code element */
|
||||
esc_html__(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||
'Die Automatik abschalten: Filter %s auf false setzen (z. B. in einem mu-Plugin). Dann gelten nicht hinterlegte Divi-Versionen bis zur Freigabe unten als ungetestet.',
|
||||
'wp-lit-mcp'
|
||||
),
|
||||
'<code>lit_mcp_divi_auto_release</code>'
|
||||
) . '</p>';
|
||||
}
|
||||
|
||||
private function stateLabel(ReleaseState $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
@@ -212,13 +342,14 @@ final class DiviTab implements AdminTab
|
||||
if ($status->state === ReleaseState::Incompatible) {
|
||||
return __(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||
'Divi hat Stellen geändert, die LIT MCP nutzt. Schreiben bleibt gesperrt, bis ein Plugin-Update von LIT MCP eingespielt ist.',
|
||||
'Divi hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Fähigkeiten (Tabelle unten) bleiben gesperrt, bis ein Plugin-Update von LIT MCP eingespielt ist; alle anderen laufen weiter.',
|
||||
'wp-lit-mcp'
|
||||
);
|
||||
}
|
||||
if ($status->state === ReleaseState::Untested) {
|
||||
return __(
|
||||
'Diese Version ist nicht geprüft. Die Divi-Werkzeuge lesen nur, bis sie freigegeben ist.',
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||
'Diese Version ist nicht geprüft. Die Werkzeuge der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
|
||||
'wp-lit-mcp'
|
||||
);
|
||||
}
|
||||
@@ -283,7 +414,8 @@ final class DiviTab implements AdminTab
|
||||
}
|
||||
}
|
||||
|
||||
if ($status->state === ReleaseState::Released && $status->source === ReleaseStatus::SOURCE_DATABASE) {
|
||||
// A database release applies whatever the overall state: one incompatible feature must not hide the way back.
|
||||
if ($status->source === ReleaseStatus::SOURCE_DATABASE || $status->release !== null) {
|
||||
$this->actionForm(self::WITHDRAW_ACTION, __('Freigabe zurückziehen', 'wp-lit-mcp'), 'secondary');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,10 @@ final class SiteStatusAbility implements Ability
|
||||
{
|
||||
return 'Überblick: Website, WordPress-Version, eigener Benutzer und Rollen, '
|
||||
. 'erkannte Integrationen (Divi, Pods, The Events Calendar). '
|
||||
. 'Bei Divi nennt problems Divi-Stellen, die nach einem Update fehlen oder sich geändert haben, und '
|
||||
. 'release, ob LIT MCP mit dieser Divi-Version schreiben darf (released, untested, incompatible).';
|
||||
. 'Bei Divi nennt problems Divi-Stellen, die nach einem Update fehlen oder sich geändert haben, '
|
||||
. 'release den Gesamtzustand und features je Fähigkeit (z. B. Seiten schreiben) Zustand '
|
||||
. '(released, untested, incompatible), Quelle (code, database, auto), Probleme und betroffene Werkzeuge. '
|
||||
. 'Werkzeuge, deren Fähigkeit nicht released ist, lehnen ab; alle anderen Divi-Werkzeuge laufen weiter.';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
@@ -49,12 +51,49 @@ final class SiteStatusAbility implements Ability
|
||||
'wordpress' => ['type' => 'string'],
|
||||
'user' => ['type' => 'object'],
|
||||
'allowed_roles' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||
'components' => ['type' => 'array'],
|
||||
'components' => ['type' => 'array', 'items' => self::componentSchema()],
|
||||
],
|
||||
'required' => ['site', 'wordpress', 'user', 'allowed_roles', 'components'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private static function componentSchema(): array
|
||||
{
|
||||
$text = ['type' => 'string'];
|
||||
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => $text,
|
||||
'active' => ['type' => 'boolean'],
|
||||
'version' => ['type' => ['string', 'null']],
|
||||
'problems' => ['type' => 'array', 'items' => $text],
|
||||
'release' => ['type' => ['object', 'null']],
|
||||
'features' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Nur bei Divi: Zustand je Fähigkeit.',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => $text,
|
||||
'label' => $text,
|
||||
'state' => ['type' => 'string', 'enum' => ['released', 'untested', 'incompatible']],
|
||||
'source' => [
|
||||
'type' => ['string', 'null'],
|
||||
'description' => 'code, database, auto oder null (nicht nutzbar).',
|
||||
],
|
||||
'problems' => ['type' => 'array', 'items' => $text],
|
||||
'tools' => ['type' => 'array', 'items' => $text],
|
||||
],
|
||||
'required' => ['id', 'label', 'state', 'source', 'problems', 'tools'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'required' => ['id', 'active', 'version', 'problems', 'release'],
|
||||
];
|
||||
}
|
||||
|
||||
public function annotations(): array
|
||||
{
|
||||
return Annotations::read();
|
||||
|
||||
@@ -6,31 +6,47 @@ namespace LIT\MCP\Integrations\Divi;
|
||||
|
||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Divi in lit-site-status: version, the internals that are missing (DiviCompatibility) and the release status
|
||||
* (spec §5.5); inactive without problems and release when Divi is not the template.
|
||||
* Divi in lit-site-status: version, missing internals (DiviCompatibility), the release status and – per feature –
|
||||
* state, source, problems and the tools it affects (E3). The features come from the matrix the release gate
|
||||
* already holds (cached, D7), so a site-status call never recomputes the mirror fingerprints.
|
||||
*/
|
||||
final class DiviEnvironmentComponent implements EnvironmentComponent
|
||||
{
|
||||
public function __construct(private ?DiviCompatibility $compatibility = null)
|
||||
private FeatureOverview $overview;
|
||||
|
||||
public function __construct(private ?DiviCompatibility $compatibility = null, ?FeatureOverview $overview = null)
|
||||
{
|
||||
$this->overview = $overview ?? FeatureOverview::withoutTools();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, active: bool, version: ?string, problems: list<string>, release: ?array<string, mixed>,
|
||||
* features: list<array{id: string, label: string, state: string, source: ?string, problems: list<string>,
|
||||
* tools: list<string>}>}
|
||||
*/
|
||||
public function describe(): array
|
||||
{
|
||||
$version = DiviEnvironment::version();
|
||||
$active = $version !== null;
|
||||
if ($version === null) {
|
||||
return ['id' => 'divi', 'active' => false, 'version' => null, 'problems' => [], 'release' => null,
|
||||
'features' => []];
|
||||
}
|
||||
|
||||
$status = ReleaseGate::status();
|
||||
|
||||
return [
|
||||
'id' => 'divi',
|
||||
'active' => $active,
|
||||
'active' => true,
|
||||
'version' => $version,
|
||||
'problems' => $active ? ($this->compatibility ?? new DiviCompatibility())->problems() : [],
|
||||
'release' => $active ? ReleaseGate::status()->toArray() : null,
|
||||
'problems' => ($this->compatibility ?? new DiviCompatibility())->problems(),
|
||||
'release' => $status->toArray(),
|
||||
'features' => $this->overview->rows($status->matrix),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\ToolAvailability;
|
||||
use LIT\MCP\Admin\DiviTab;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
@@ -55,6 +56,7 @@ use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviCatalogAbility;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
@@ -95,11 +97,14 @@ final class DiviIntegration implements Integration, Bootable
|
||||
* "Divi Theme Builder" gate their entries in the Divi tools (ruling P9, E9), lit/divi-insert gates
|
||||
* lit/divi-node-copy, lit-library-insert and content-create's divi input (audit M2), lit/library-insert
|
||||
* gates lit/divi-node-copy from a library entry (audit L5). null = no Tools tab: everything allowed.
|
||||
* @param (Closure(): list<Ability>)|null $allAbilities The abilities of every integration (Plugin passes
|
||||
* IntegrationRegistry::abilities()); the tools a Divi feature affects are found among them (E3).
|
||||
*/
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private ChangeLog $log,
|
||||
private ?ToolAvailability $tools = null
|
||||
private ?ToolAvailability $tools = null,
|
||||
private ?Closure $allAbilities = null
|
||||
) {
|
||||
$this->modules = new DiviModules();
|
||||
$this->design = new DesignSystemReader();
|
||||
@@ -177,10 +182,16 @@ final class DiviIntegration implements Integration, Bootable
|
||||
return [new DiviLogoUsage()];
|
||||
}
|
||||
|
||||
/** Port for lit-site-status: Divi's version, missing internals and release status. */
|
||||
/** Which tools each Divi feature affects – for lit-site-status and the Divi tab (E3). */
|
||||
public function featureOverview(): FeatureOverview
|
||||
{
|
||||
return new FeatureOverview($this->allAbilities ?? static fn (): array => []);
|
||||
}
|
||||
|
||||
/** Port for lit-site-status: Divi's version, missing internals, release status and features. */
|
||||
public function environmentComponent(): EnvironmentComponent
|
||||
{
|
||||
return new DiviEnvironmentComponent();
|
||||
return new DiviEnvironmentComponent(overview: $this->featureOverview());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +221,10 @@ final class DiviIntegration implements Integration, Bootable
|
||||
(new DiviCheckRoute($check))->register();
|
||||
DiviCheckCommand::register($check);
|
||||
|
||||
add_filter(SettingsPage::TABS_FILTER, fn (array $tabs): array => [...$tabs, new DiviTab(log: $this->log)]);
|
||||
add_filter(
|
||||
SettingsPage::TABS_FILTER,
|
||||
fn (array $tabs): array => [...$tabs, new DiviTab(log: $this->log, overview: $this->featureOverview())]
|
||||
);
|
||||
}
|
||||
|
||||
public function abilities(): array
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The per-feature view shared by lit-site-status and the admin tab (E3): each row of the compatibility matrix plus
|
||||
* the tools that need the feature. Reads the matrix the release gate already holds (cached, D7) – nothing is
|
||||
* checked again here.
|
||||
*/
|
||||
final class FeatureOverview
|
||||
{
|
||||
/** @param Closure(): list<Ability> $abilities All registered abilities (IntegrationRegistry::abilities()). */
|
||||
public function __construct(private Closure $abilities)
|
||||
{
|
||||
}
|
||||
|
||||
/** An overview that knows no tools (tests, and a Divi tab built without a registry). */
|
||||
public static function withoutTools(): self
|
||||
{
|
||||
return new self(static fn (): array => []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tools whose diviFeatures([]) contain the feature, named as the MCP client sees them: direct tools
|
||||
* lit-<name>, indirect ones lit/<name>. A tool that needs a feature only for certain input (content-create
|
||||
* with divi) is not listed – its refusal names the feature.
|
||||
*
|
||||
* @return array<string, list<string>> Feature ID => tool names.
|
||||
*/
|
||||
public function toolsByFeature(): array
|
||||
{
|
||||
$map = [];
|
||||
foreach (($this->abilities)() as $ability) {
|
||||
if (! $ability instanceof RequiresDiviFeatures) {
|
||||
continue;
|
||||
}
|
||||
foreach ($ability->diviFeatures([]) as $feature) {
|
||||
$map[$feature->value][] = $ability->isDirect()
|
||||
? str_replace('/', '-', $ability->name())
|
||||
: $ability->name();
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: string, label: string, state: string, source: ?string, problems: list<string>,
|
||||
* tools: list<string>}>
|
||||
*/
|
||||
public function rows(CompatibilityMatrix $matrix): array
|
||||
{
|
||||
$tools = $this->toolsByFeature();
|
||||
|
||||
return array_map(
|
||||
static fn (array $row): array => $row + ['tools' => $tools[$row['id']] ?? []],
|
||||
$matrix->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -131,9 +131,15 @@ final class Plugin
|
||||
Limits $limits,
|
||||
ToolAvailability $tools
|
||||
): IntegrationRegistry {
|
||||
$divi = new DiviIntegration($schema, $changeLog, $tools);
|
||||
// E3: lit-site-status and the Divi tab name the tools each feature affects; the registry exists only after
|
||||
// the Divi integration, so it is asked at call time.
|
||||
$registry = null;
|
||||
$allAbilities = static function () use (&$registry): array {
|
||||
return $registry instanceof IntegrationRegistry ? $registry->abilities() : [];
|
||||
};
|
||||
$divi = new DiviIntegration($schema, $changeLog, $tools, $allAbilities);
|
||||
|
||||
return new IntegrationRegistry(
|
||||
$registry = new IntegrationRegistry(
|
||||
new CoreIntegration(self::pluginDir() . '/docs/guides', [$divi->environmentComponent()]),
|
||||
new WordPressIntegration(
|
||||
$schema,
|
||||
@@ -147,6 +153,8 @@ final class Plugin
|
||||
$divi->library(),
|
||||
$divi->themeBuilder()
|
||||
);
|
||||
|
||||
return $registry;
|
||||
}
|
||||
|
||||
public static function settingsPage(): ?SettingsPage
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Support;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
/** A tool that needs the given Divi features – for the feature overview, the tab and site status. */
|
||||
final class FeatureFakeTool implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
/** @param list<\LIT\MCP\Integrations\Divi\Release\DiviFeature> $features */
|
||||
public function __construct(
|
||||
private string $name,
|
||||
private bool $direct,
|
||||
private array $features
|
||||
) {
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
{
|
||||
return ['type' => 'object'];
|
||||
}
|
||||
|
||||
public function outputSchema(): ?array
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function annotations(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function isDirect(): bool
|
||||
{
|
||||
return $this->direct;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function execute(array $input): mixed
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return $this->features;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Support;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
|
||||
/** Compatibility matrices for tests: every feature released, with single features replaced. */
|
||||
final class MatrixFixtures
|
||||
{
|
||||
/**
|
||||
* @param list<FeatureStatus> $replace Statuses that replace the released one of their feature.
|
||||
*/
|
||||
public static function released(string $source = 'code', array $replace = []): CompatibilityMatrix
|
||||
{
|
||||
$features = [];
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
$features[$feature->value] = new FeatureStatus($feature, ReleaseState::Released, [], $source);
|
||||
}
|
||||
foreach ($replace as $status) {
|
||||
$features[$status->feature->value] = $status;
|
||||
}
|
||||
|
||||
return new CompatibilityMatrix($features);
|
||||
}
|
||||
|
||||
/** @param string ...$problems */
|
||||
public static function with(
|
||||
DiviFeature $feature,
|
||||
ReleaseState $state,
|
||||
string $source = 'code',
|
||||
string ...$problems
|
||||
): CompatibilityMatrix {
|
||||
return self::released($source, [new FeatureStatus($feature, $state, $problems, null)]);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ use LIT\MCP\Admin\DiviTab;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckStep;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
@@ -18,6 +21,8 @@ use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
||||
use LIT\MCP\Plugin;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Support\FeatureFakeTool;
|
||||
use LIT\MCP\Tests\Support\MatrixFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use WP_Post;
|
||||
@@ -63,8 +68,12 @@ final class DiviTabTest extends TestCase
|
||||
return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, $version, Plugin::VERSION, $key, $steps);
|
||||
}
|
||||
|
||||
private function tab(?CheckReport $report, ReleaseStore $releases, ?FakeChangeLog $log = null): DiviTab
|
||||
{
|
||||
private function tab(
|
||||
?CheckReport $report,
|
||||
ReleaseStore $releases,
|
||||
?FakeChangeLog $log = null,
|
||||
?FeatureOverview $overview = null
|
||||
): DiviTab {
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->allows('latest')->andReturn($report);
|
||||
|
||||
@@ -75,7 +84,8 @@ final class DiviTabTest extends TestCase
|
||||
new AdminPostAction(static function (): void {
|
||||
}),
|
||||
static fn (): string => '5.3.0',
|
||||
static fn (string $version): string => 'key'
|
||||
static fn (string $version): string => 'key',
|
||||
$overview
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,6 +164,19 @@ final class DiviTabTest extends TestCase
|
||||
|
||||
/** @return string The rendered tab. */
|
||||
private function rendered(ReleaseStatus $status, ?CheckReport $report, ?DiviTab $tab = null): string
|
||||
{
|
||||
return $this->renderedTab($status, $tab ?? $this->tab($report, Mockery::mock(ReleaseStore::class)));
|
||||
}
|
||||
|
||||
private function renderedWith(ReleaseStatus $status, ?CheckReport $report, FeatureOverview $overview): string
|
||||
{
|
||||
return $this->renderedTab(
|
||||
$status,
|
||||
$this->tab($report, Mockery::mock(ReleaseStore::class), null, $overview)
|
||||
);
|
||||
}
|
||||
|
||||
private function renderedTab(ReleaseStatus $status, DiviTab $tab): string
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||
Functions\when('rest_url')->justReturn('https://k.de/wp-json/lit-mcp/v1/divi-check');
|
||||
@@ -167,7 +190,7 @@ final class DiviTabTest extends TestCase
|
||||
Functions\when('get_option')->justReturn('d.m.Y');
|
||||
|
||||
ob_start();
|
||||
($tab ?? $this->tab($report, Mockery::mock(ReleaseStore::class)))->render();
|
||||
$tab->render();
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
@@ -331,4 +354,191 @@ final class DiviTabTest extends TestCase
|
||||
static fn (): string => ''
|
||||
))->removeOrphans();
|
||||
}
|
||||
|
||||
private static function mixedStatus(): ReleaseStatus
|
||||
{
|
||||
$matrix = MatrixFixtures::released('auto', [
|
||||
new FeatureStatus(
|
||||
DiviFeature::PageWrite,
|
||||
ReleaseState::Incompatible,
|
||||
['Seiten: Signatur von et_pb_x() <geändert>'],
|
||||
null
|
||||
),
|
||||
new FeatureStatus(DiviFeature::Catalog, ReleaseState::Untested, [], null),
|
||||
]);
|
||||
|
||||
return new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, [], null, matrix: $matrix);
|
||||
}
|
||||
|
||||
public function testTheFeatureTableShowsStateSourceProblemsAndTools(): void
|
||||
{
|
||||
$overview = new FeatureOverview(static fn (): array => [
|
||||
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageRead, DiviFeature::PageWrite]),
|
||||
new FeatureFakeTool('lit/divi-presets-assign', false, [DiviFeature::PresetWrite]),
|
||||
]);
|
||||
|
||||
$html = $this->renderedWith(self::mixedStatus(), null, $overview);
|
||||
|
||||
self::assertStringContainsString('<th scope="col">Betroffene Werkzeuge</th>', $html);
|
||||
self::assertStringContainsString(
|
||||
'<td>Seiten schreiben</td><td><strong>inkompatibel</strong></td><td>–</td>',
|
||||
$html
|
||||
);
|
||||
self::assertStringContainsString('<td>Modulkatalog</td><td><strong>ungetestet</strong></td>', $html);
|
||||
self::assertStringContainsString(
|
||||
'<td>Seiten lesen</td><td><strong>freigegeben</strong></td><td>Automatik (auto)</td>',
|
||||
$html
|
||||
);
|
||||
self::assertStringContainsString('<li><code>lit-divi-insert</code></li>', $html);
|
||||
self::assertStringContainsString('<li><code>lit/divi-presets-assign</code></li>', $html);
|
||||
}
|
||||
|
||||
public function testFeatureProblemsAreEscaped(): void
|
||||
{
|
||||
$html = $this->rendered(self::mixedStatus(), null);
|
||||
|
||||
self::assertStringContainsString('Seiten: Signatur von et_pb_x() <geändert>', $html);
|
||||
self::assertStringNotContainsString('<geändert>', $html);
|
||||
}
|
||||
|
||||
public function testTheTableNamesEverySourceInWords(): void
|
||||
{
|
||||
$code = $this->rendered(ReleaseStatus::released('5.3.0'), null);
|
||||
$database = $this->rendered(
|
||||
new ReleaseStatus(ReleaseState::Released, '5.3.0', ReleaseStatus::SOURCE_DATABASE, [], [
|
||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
||||
], matrix: MatrixFixtures::released('database')),
|
||||
self::report()
|
||||
);
|
||||
|
||||
self::assertStringContainsString('Plugin (code)', $code);
|
||||
self::assertStringContainsString('Freigabe auf dieser Website (database)', $database);
|
||||
}
|
||||
|
||||
public function testTheTabExplainsTheAutomationAndTheFilter(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseStatus::released('5.3.0'), null);
|
||||
|
||||
self::assertStringContainsString('gibt LIT MCP jede Fähigkeit einzeln frei', $html);
|
||||
self::assertStringContainsString('<code>lit_mcp_divi_auto_release</code>', $html);
|
||||
self::assertStringContainsString('als ungetestet', $html);
|
||||
self::assertStringContainsString('Aufrufe', $html);
|
||||
self::assertStringNotContainsString('Verhalten geprüft', $html);
|
||||
}
|
||||
|
||||
public function testTheTabNoLongerSaysWritingIsLockedAsAWhole(): void
|
||||
{
|
||||
$untested = $this->rendered(
|
||||
new ReleaseStatus(
|
||||
ReleaseState::Untested,
|
||||
'5.3.0',
|
||||
matrix: MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Untested)
|
||||
),
|
||||
null
|
||||
);
|
||||
$incompatible = $this->rendered(self::mixedStatus(), null);
|
||||
|
||||
foreach ([$untested, $incompatible] as $html) {
|
||||
self::assertStringNotContainsString('Schreiben bleibt gesperrt', $html);
|
||||
self::assertStringNotContainsString('lesen nur', $html);
|
||||
self::assertStringContainsString('alle anderen laufen weiter', $html);
|
||||
}
|
||||
}
|
||||
|
||||
public function testADatabaseReleaseCanBeWithdrawnWhileOneFeatureIsIncompatible(): void
|
||||
{
|
||||
$matrix = MatrixFixtures::released('database', [
|
||||
new FeatureStatus(DiviFeature::PageWrite, ReleaseState::Incompatible, ['Seiten: x() fehlt'], null),
|
||||
]);
|
||||
$status = new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', ReleaseStatus::SOURCE_DATABASE, [], [
|
||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
||||
], matrix: $matrix);
|
||||
|
||||
self::assertStringContainsString('Freigabe zurückziehen', $this->rendered($status, self::report()));
|
||||
}
|
||||
|
||||
public function testWithoutADatabaseReleaseNothingCanBeWithdrawnWhateverTheState(): void
|
||||
{
|
||||
self::assertStringNotContainsString('Freigabe zurückziehen', $this->rendered(self::mixedStatus(), null));
|
||||
}
|
||||
|
||||
/** @return string The admin notice of this request. */
|
||||
private function notice(ReleaseStatus $status, string $version = '5.3.0'): string
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||
$tab = new DiviTab(
|
||||
Mockery::mock(CheckReportStore::class),
|
||||
Mockery::mock(ReleaseStore::class),
|
||||
FakeChangeLog::forbidden(),
|
||||
new AdminPostAction(static function (): void {
|
||||
}),
|
||||
static fn () => $version,
|
||||
static fn (string $v): string => 'key'
|
||||
);
|
||||
|
||||
ob_start();
|
||||
$tab->renderNotice();
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
public function testTheNoticeNamesTheUnusableFeaturesAndLinksToTheTab(): void
|
||||
{
|
||||
$html = $this->notice(self::mixedStatus());
|
||||
|
||||
self::assertStringContainsString('class="notice notice-warning"', $html);
|
||||
self::assertStringContainsString('Mit Divi 5.3.0 sind diese Fähigkeiten nicht nutzbar', $html);
|
||||
self::assertStringContainsString('Seiten schreiben, Modulkatalog', $html);
|
||||
self::assertStringContainsString('alle anderen laufen weiter', $html);
|
||||
self::assertStringContainsString('page=lit-mcp&tab=divi', $html);
|
||||
}
|
||||
|
||||
public function testNoNoticeWhileEveryFeatureIsUsable(): void
|
||||
{
|
||||
self::assertSame('', $this->notice(ReleaseStatus::released('5.3.0')));
|
||||
}
|
||||
|
||||
public function testNoNoticeForUsersWithoutManageOptions(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
self::assertSame('', $this->notice(self::mixedStatus()));
|
||||
}
|
||||
|
||||
public function testNoNoticeWithoutDivi(): void
|
||||
{
|
||||
self::assertSame('', $this->notice(self::mixedStatus(), ''));
|
||||
}
|
||||
|
||||
public function testNoNoticeOnTheDiviTabItself(): void
|
||||
{
|
||||
Functions\when('get_current_screen')->justReturn((object) ['id' => 'settings_page_lit-mcp']);
|
||||
$_GET['tab'] = 'divi';
|
||||
|
||||
self::assertSame('', $this->notice(self::mixedStatus()));
|
||||
}
|
||||
|
||||
public function testTheNoticeStillShowsOnOtherTabsOfThePluginPage(): void
|
||||
{
|
||||
Functions\when('get_current_screen')->justReturn((object) ['id' => 'settings_page_lit-mcp']);
|
||||
$_GET['tab'] = 'tools';
|
||||
|
||||
self::assertStringContainsString('nicht nutzbar', $this->notice(self::mixedStatus()));
|
||||
}
|
||||
|
||||
public function testTheNoticeIsRegisteredOnAdminNotices(): void
|
||||
{
|
||||
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
|
||||
Actions\expectAdded('admin_notices')->once()->with([$tab, 'renderNotice']);
|
||||
|
||||
$tab->registerNotice();
|
||||
}
|
||||
|
||||
public function testRegisteringTheTabsActionsAlsoRegistersTheNotice(): void
|
||||
{
|
||||
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
|
||||
Actions\expectAdded('admin_notices')->once()->with([$tab, 'renderNotice']);
|
||||
|
||||
$tab->registerActions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,28 @@ final class SiteStatusAbilityTest extends TestCase
|
||||
self::assertSame(['administrator', 'editor'], $result['allowed_roles']);
|
||||
self::assertSame('divi', $result['components'][0]['id']);
|
||||
}
|
||||
|
||||
public function testTheOutputSchemaDescribesTheDiviFeatures(): void
|
||||
{
|
||||
$schema = (new SiteStatusAbility(Mockery::mock(EnvironmentProbe::class), new Access()))->outputSchema();
|
||||
$component = $schema['properties']['components']['items'];
|
||||
$feature = $component['properties']['features']['items'];
|
||||
|
||||
self::assertSame(['id', 'active', 'version', 'problems', 'release'], $component['required']);
|
||||
self::assertSame(
|
||||
['id', 'label', 'state', 'source', 'problems', 'tools'],
|
||||
$feature['required']
|
||||
);
|
||||
self::assertSame(['released', 'untested', 'incompatible'], $feature['properties']['state']['enum']);
|
||||
self::assertSame(['string', 'null'], $feature['properties']['source']['type']);
|
||||
}
|
||||
|
||||
public function testTheDescriptionNamesTheFeaturesAndSaysOtherToolsKeepWorking(): void
|
||||
{
|
||||
$description = (new SiteStatusAbility(Mockery::mock(EnvironmentProbe::class), new Access()))->description();
|
||||
|
||||
self::assertStringContainsString('features je Fähigkeit', $description);
|
||||
self::assertStringContainsString('(code, database, auto)', $description);
|
||||
self::assertStringContainsString('alle anderen Divi-Werkzeuge laufen weiter', $description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,13 @@ use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||
use LIT\MCP\Integrations\Divi\DiviEnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Tests\Support\FeatureFakeTool;
|
||||
use LIT\MCP\Tests\Support\MatrixFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
@@ -61,8 +68,78 @@ final class DiviEnvironmentComponentTest extends TestCase
|
||||
))->describe();
|
||||
|
||||
self::assertSame(
|
||||
['id' => 'divi', 'active' => false, 'version' => null, 'problems' => [], 'release' => null],
|
||||
['id' => 'divi', 'active' => false, 'version' => null, 'problems' => [], 'release' => null,
|
||||
'features' => []],
|
||||
$described
|
||||
);
|
||||
}
|
||||
|
||||
public function testListsEveryFeatureWithStateSourceProblemsAndTools(): void
|
||||
{
|
||||
$this->theme('Divi', '5.3.0');
|
||||
$matrix = MatrixFixtures::with(
|
||||
DiviFeature::PageWrite,
|
||||
ReleaseState::Incompatible,
|
||||
'auto',
|
||||
'Seiten: Signatur von et_pb_x() geändert'
|
||||
);
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(
|
||||
ReleaseState::Incompatible,
|
||||
'5.3.0',
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
matrix: $matrix
|
||||
));
|
||||
$overview = new FeatureOverview(static fn (): array => [
|
||||
new FeatureFakeTool('lit/divi-outline', true, [DiviFeature::PageRead]),
|
||||
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageRead, DiviFeature::PageWrite]),
|
||||
]);
|
||||
|
||||
$component = (new DiviEnvironmentComponent(new DiviCompatibility([]), $overview))->describe();
|
||||
$features = array_column($component['features'], null, 'id');
|
||||
|
||||
self::assertSame('divi', $component['id']);
|
||||
self::assertCount(count(DiviFeature::cases()), $features);
|
||||
self::assertSame('incompatible', $features['page-write']['state']);
|
||||
self::assertNull($features['page-write']['source']);
|
||||
self::assertSame(['Seiten: Signatur von et_pb_x() geändert'], $features['page-write']['problems']);
|
||||
self::assertSame(['lit-divi-insert'], $features['page-write']['tools']);
|
||||
self::assertSame('Seiten schreiben', $features['page-write']['label']);
|
||||
self::assertSame('released', $features['page-read']['state']);
|
||||
self::assertSame('auto', $features['page-read']['source']);
|
||||
self::assertSame(['lit-divi-outline', 'lit-divi-insert'], $features['page-read']['tools']);
|
||||
self::assertSame('incompatible', $component['release']['state']);
|
||||
}
|
||||
|
||||
public function testTheMatrixComesFromTheGateAndIsNotComputedAgain(): void
|
||||
{
|
||||
$this->theme('Divi', '5.3.0');
|
||||
$calls = 0;
|
||||
ReleaseGate::useResolver(static function () use (&$calls): ReleaseStatus {
|
||||
++$calls;
|
||||
|
||||
return ReleaseStatus::released('5.3.0');
|
||||
});
|
||||
$component = new DiviEnvironmentComponent(new DiviCompatibility([]));
|
||||
|
||||
$component->describe();
|
||||
$component->describe();
|
||||
|
||||
self::assertSame(1, $calls);
|
||||
}
|
||||
|
||||
public function testWithoutDiviThereAreNoFeaturesAndNoGateCall(): void
|
||||
{
|
||||
$this->theme('twentytwentyfive', '1.0');
|
||||
ReleaseGate::useResolver(static function (): ReleaseStatus {
|
||||
self::fail('the gate must not be asked without Divi');
|
||||
});
|
||||
|
||||
$component = (new DiviEnvironmentComponent(new DiviCompatibility([])))->describe();
|
||||
|
||||
self::assertFalse($component['active']);
|
||||
self::assertSame([], $component['features']);
|
||||
self::assertNull($component['release']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use LIT\MCP\Integrations\Divi\DiviIntegration;
|
||||
use LIT\MCP\Integrations\Divi\DiviLogoUsage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
@@ -27,6 +28,7 @@ use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||||
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Support\FeatureFakeTool;
|
||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
@@ -327,4 +329,28 @@ final class DiviIntegrationTest extends TestCase
|
||||
self::assertSame($base, $all[0]);
|
||||
self::assertInstanceOf(DiviTab::class, $all[1]);
|
||||
}
|
||||
|
||||
public function testTheFeatureOverviewFindsTheToolsOfAllIntegrations(): void
|
||||
{
|
||||
$integration = new DiviIntegration(
|
||||
Mockery::mock(ContentSchema::class),
|
||||
FakeChangeLog::forbidden(),
|
||||
allAbilities: static fn (): array => [
|
||||
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageWrite]),
|
||||
new FeatureFakeTool('lit/library-insert', true, [DiviFeature::PageWrite, DiviFeature::LibraryRead]),
|
||||
]
|
||||
);
|
||||
|
||||
self::assertSame(
|
||||
['page-write' => ['lit-divi-insert', 'lit-library-insert'], 'library-read' => ['lit-library-insert']],
|
||||
$integration->featureOverview()->toolsByFeature()
|
||||
);
|
||||
}
|
||||
|
||||
public function testWithoutAnAbilityListTheOverviewKnowsNoTools(): void
|
||||
{
|
||||
$integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden());
|
||||
|
||||
self::assertSame([], $integration->featureOverview()->toolsByFeature());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Tests\Support\FeatureFakeTool;
|
||||
use LIT\MCP\Tests\Support\MatrixFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FeatureOverviewTest extends TestCase
|
||||
{
|
||||
private static function overview(): FeatureOverview
|
||||
{
|
||||
return new FeatureOverview(static fn (): array => [
|
||||
new FeatureFakeTool('lit/divi-outline', true, [DiviFeature::PageRead]),
|
||||
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageRead, DiviFeature::PageWrite]),
|
||||
new FeatureFakeTool('lit/divi-presets-assign', false, [DiviFeature::PresetWrite]),
|
||||
new FeatureFakeTool('lit/content-create', true, []),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testToolsAreNamedAsTheClientSeesThem(): void
|
||||
{
|
||||
$tools = self::overview()->toolsByFeature();
|
||||
|
||||
self::assertSame(['lit-divi-outline', 'lit-divi-insert'], $tools['page-read']);
|
||||
self::assertSame(['lit-divi-insert'], $tools['page-write']);
|
||||
self::assertSame(['lit/divi-presets-assign'], $tools['preset-write']);
|
||||
}
|
||||
|
||||
public function testAToolWithoutFeaturesIsLeftOut(): void
|
||||
{
|
||||
$overview = new FeatureOverview(static fn (): array => [new FeatureFakeTool('lit/content-create', true, [])]);
|
||||
|
||||
self::assertSame([], $overview->toolsByFeature());
|
||||
}
|
||||
|
||||
public function testRowsCarryTheMatrixAndTheToolsOfEachFeature(): void
|
||||
{
|
||||
$matrix = MatrixFixtures::with(
|
||||
DiviFeature::PageWrite,
|
||||
ReleaseState::Incompatible,
|
||||
'code',
|
||||
'Seiten: et_pb_x() fehlt'
|
||||
);
|
||||
|
||||
$rows = array_column(self::overview()->rows($matrix), null, 'id');
|
||||
|
||||
self::assertCount(count(DiviFeature::cases()), $rows);
|
||||
self::assertSame(
|
||||
[
|
||||
'id' => 'page-write',
|
||||
'label' => 'Seiten schreiben',
|
||||
'state' => 'incompatible',
|
||||
'problems' => ['Seiten: et_pb_x() fehlt'],
|
||||
'source' => null,
|
||||
'tools' => ['lit-divi-insert'],
|
||||
],
|
||||
$rows['page-write']
|
||||
);
|
||||
self::assertSame('released', $rows['page-read']['state']);
|
||||
self::assertSame('code', $rows['page-read']['source']);
|
||||
self::assertSame([], $rows['catalog']['tools']);
|
||||
}
|
||||
|
||||
public function testWithoutToolsEveryRowHasAnEmptyToolList(): void
|
||||
{
|
||||
foreach (FeatureOverview::withoutTools()->rows(MatrixFixtures::released()) as $row) {
|
||||
self::assertSame([], $row['tools']);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user