refactor(divi): DiviTab geht im Reiter „Compatibility“ auf
Der eigene Divi-Reiter entfällt. DiviAdminExtras behält Admin-Hinweis, Orphan-Bereinigung vor der Ausgabe (lit_mcp_compat_profile_load), Hinweis zum nächsten Schritt und zur privaten Testseite (lit_mcp_compat_profile_intro), Zustandstexte mit Quelle (lit_mcp_compat_explanation), Automatik-Notiz und Visual-Builder-Hinweis (lit_mcp_compat_profile_extra) sowie die Spalten Quelle, Probleme und Betroffene Tools über die neue, gegen fehlerhafte Callbacks abgesicherte Erweiterung lit_mcp_compat_capability_columns. Texte, Doku, CSS (lit-mcp-compat-*) und Smoke auf den neuen Reiter umgestellt, tote divi_*-Hinweise entfernt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
85f4138b36
commit
b40b81f585
+16
-16
@@ -132,13 +132,13 @@
|
|||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Divi tab: the report is a plain list, the icon replaces the bullet. */
|
/* Tab "Compatibility": the report is a plain list, the icon replaces the bullet. */
|
||||||
.lit-mcp-divi-report {
|
.lit-mcp-compat-report {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-divi-report ul {
|
.lit-mcp-compat-report ul {
|
||||||
list-style: disc;
|
list-style: disc;
|
||||||
margin-left: 2em;
|
margin-left: 2em;
|
||||||
}
|
}
|
||||||
@@ -157,12 +157,12 @@
|
|||||||
|
|
||||||
/* Folded help ("Wo finde ich das?") and the Divi detail table. */
|
/* Folded help ("Wo finde ich das?") and the Divi detail table. */
|
||||||
.lit-mcp-hint,
|
.lit-mcp-hint,
|
||||||
.lit-mcp-divi-details {
|
.lit-mcp-compat-details {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-hint summary,
|
.lit-mcp-hint summary,
|
||||||
.lit-mcp-divi-details summary {
|
.lit-mcp-compat-details summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-divi-details summary {
|
.lit-mcp-compat-details summary {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 1em 0 0.5em;
|
margin: 1em 0 0.5em;
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-hint summary,
|
.lit-mcp-hint summary,
|
||||||
.lit-mcp-divi-details summary {
|
.lit-mcp-compat-details summary {
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,9 +435,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Divi tab: on phones each row of the feature table becomes a card named by data-label. */
|
/* Tab "Compatibility": on phones each row of the feature table becomes a card named by data-label. */
|
||||||
@media screen and (max-width: 782px) {
|
@media screen and (max-width: 782px) {
|
||||||
.lit-mcp-divi-features thead {
|
.lit-mcp-compat-features thead {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -445,31 +445,31 @@
|
|||||||
clip-path: inset(50%);
|
clip-path: inset(50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-divi-features tr,
|
.lit-mcp-compat-features tr,
|
||||||
.lit-mcp-divi-features td {
|
.lit-mcp-compat-features td {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* One card per row: a rule and room between them, the feature name first and bold. */
|
/* One card per row: a rule and room between them, the feature name first and bold. */
|
||||||
.lit-mcp-divi-features tr {
|
.lit-mcp-compat-features tr {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid #c3c4c7;
|
border-bottom: 1px solid #c3c4c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-divi-features td:first-child {
|
.lit-mcp-compat-features td:first-child {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-divi-features code {
|
.lit-mcp-compat-features code {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-divi-features td::before {
|
.lit-mcp-compat-features td::before {
|
||||||
content: attr(data-label) ": ";
|
content: attr(data-label) ": ";
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lit-mcp-divi-features ul {
|
.lit-mcp-compat-features ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ nichts geändert. Vorgehen:
|
|||||||
Werkzeuge (`tools`).
|
Werkzeuge (`tools`).
|
||||||
3. Ein anderes Werkzeug nutzen, das die betroffene Fähigkeit nicht braucht – ist z. B. nur „Seiten schreiben“
|
3. Ein anderes Werkzeug nutzen, das die betroffene Fähigkeit nicht braucht – ist z. B. nur „Seiten schreiben“
|
||||||
betroffen, funktioniert `lit-divi-outline` weiter, und der Nutzer kann die Änderung selbst im Divi-Builder machen.
|
betroffen, funktioniert `lit-divi-outline` weiter, und der Nutzer kann die Änderung selbst im Divi-Builder machen.
|
||||||
4. Den Nutzer informieren: bei `divi_untested` soll ein Administrator unter Einstellungen → LIT MCP → Divi die
|
4. Den Nutzer informieren: bei `divi_untested` soll ein Administrator unter Einstellungen → LIT MCP → Compatibility die
|
||||||
Prüfung ausführen und die Version freigeben; bei `divi_incompatible` muss LIT MCP aktualisiert werden. Die Sperre
|
Prüfung ausführen und die Version freigeben; bei `divi_incompatible` muss LIT MCP aktualisiert werden. Die Sperre
|
||||||
nicht über andere Werkzeuge umgehen.
|
nicht über andere Werkzeuge umgehen.
|
||||||
|
5. Dasselbe gilt für Pods: Ist eine Pods-Fähigkeit (`pods-…`) nicht nutzbar, antwortet das betroffene Werkzeug mit
|
||||||
|
`compat_incompatible`; der Stand steht im selben Reiter „Compatibility“.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ insgesamt:
|
|||||||
2. Mit Werkzeugen weiterarbeiten, deren Fähigkeit `released` ist (z. B. Seiten lesen mit `lit-divi-outline`), und den
|
2. Mit Werkzeugen weiterarbeiten, deren Fähigkeit `released` ist (z. B. Seiten lesen mit `lit-divi-outline`), und den
|
||||||
gesperrten Teil auslassen.
|
gesperrten Teil auslassen.
|
||||||
3. Dem Nutzer sagen, welche Fähigkeit gesperrt ist: bei `untested` muss ein Administrator unter Einstellungen → LIT
|
3. Dem Nutzer sagen, welche Fähigkeit gesperrt ist: bei `untested` muss ein Administrator unter Einstellungen → LIT
|
||||||
MCP → Divi die Prüfung ausführen und freigeben, bei `incompatible` muss LIT MCP aktualisiert werden.
|
MCP → Compatibility die Prüfung ausführen und freigeben, bei `incompatible` muss LIT MCP aktualisiert werden.
|
||||||
|
|
||||||
## Bibliothek und globale Module
|
## Bibliothek und globale Module
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -205,7 +205,7 @@ Weg nicht: der Wechsel auf 1.0.2 ist einmalig von Hand (ZIP hochladen), danach l
|
|||||||
|
|
||||||
== Divi-Freigabe ==
|
== Divi-Freigabe ==
|
||||||
|
|
||||||
LIT MCP schreibt nur mit Divi-Versionen, die für diese Plugin-Version geprüft sind. Nach einem Divi-Update lesen die Divi-Werkzeuge weiter; schreibende Divi-Werkzeuge antworten mit „divi_untested“, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung startet und die Version freigibt. Die Prüfung legt eine Entwurfsseite an, schreibt und rendert sie und löscht sie danach endgültig. Hat Divi Stellen geändert, die LIT MCP nachbildet, ist die Version „inkompatibel“ (divi_incompatible) – dann hilft nur ein Update von LIT MCP. Für Entwickler: `wp --user=<admin> lit-mcp divi-check [--diff] [--update-reference]`.
|
LIT MCP schreibt nur mit Divi-Versionen, die für diese Plugin-Version geprüft sind. Nach einem Divi-Update lesen die Divi-Werkzeuge weiter; schreibende Divi-Werkzeuge antworten mit „divi_untested“, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility die Prüfung startet und die Version freigibt. Die Prüfung legt eine Entwurfsseite an, schreibt und rendert sie und löscht sie danach endgültig. Hat Divi Stellen geändert, die LIT MCP nachbildet, ist die Version „inkompatibel“ (divi_incompatible) – dann hilft nur ein Update von LIT MCP. Für Entwickler: `wp --user=<admin> lit-mcp divi-check [--diff] [--update-reference]`.
|
||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
|||||||
+100
-10
@@ -6,6 +6,7 @@ namespace LIT\MCP\Admin;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\ChangeLog\ChangeLog;
|
use LIT\MCP\ChangeLog\ChangeLog;
|
||||||
|
use LIT\MCP\Compatibility\Capability;
|
||||||
use LIT\MCP\Compatibility\CheckReport;
|
use LIT\MCP\Compatibility\CheckReport;
|
||||||
use LIT\MCP\Compatibility\CheckReportStore;
|
use LIT\MCP\Compatibility\CheckReportStore;
|
||||||
use LIT\MCP\Compatibility\Compatibility;
|
use LIT\MCP\Compatibility\Compatibility;
|
||||||
@@ -14,7 +15,9 @@ use LIT\MCP\Compatibility\Profile;
|
|||||||
use LIT\MCP\Compatibility\ReleaseState;
|
use LIT\MCP\Compatibility\ReleaseState;
|
||||||
use LIT\MCP\Compatibility\ReleaseStore;
|
use LIT\MCP\Compatibility\ReleaseStore;
|
||||||
use LIT\MCP\Compatibility\Tier;
|
use LIT\MCP\Compatibility\Tier;
|
||||||
|
use LIT\MCP\Log;
|
||||||
use LIT\MCP\Plugin;
|
use LIT\MCP\Plugin;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
@@ -23,14 +26,27 @@ defined('ABSPATH') || exit;
|
|||||||
* (word and icon, never colour alone), what the tier means for a release, the capabilities, tested and released
|
* (word and icon, never colour alone), what the tier means for a release, the capabilities, tested and released
|
||||||
* versions, the latest check report and "Prüfung starten" (Admin\CheckRoute). A profile that is not active is one
|
* versions, the latest check report and "Prüfung starten" (Admin\CheckRoute). A profile that is not active is one
|
||||||
* line and never asks for its matrix. Everything that comes from a third-party plugin (version, problem texts, step
|
* line and never asks for its matrix. Everything that comes from a third-party plugin (version, problem texts, step
|
||||||
* labels) is escaped on output. The action lit_mcp_compat_profile_extra($id) closes a section: profiles add their
|
* labels) is escaped on output. Profiles hook in: lit_mcp_compat_profile_load($id) runs before any output (see
|
||||||
* own notes there; the action lit_mcp_compat_profile_load($id) (see loadProfiles()) runs before any output.
|
* loadProfiles()), lit_mcp_compat_profile_intro($id) opens the section above the capability table (next step,
|
||||||
|
* notices), lit_mcp_compat_profile_extra($id) closes it, and the filter lit_mcp_compat_explanation($text, $id,
|
||||||
|
* ReleaseState) replaces the generic sentence next to the overall state.
|
||||||
*/
|
*/
|
||||||
final class CompatibilityTab implements AdminTab
|
final class CompatibilityTab implements AdminTab
|
||||||
{
|
{
|
||||||
public const SLUG = 'compatibility';
|
public const SLUG = 'compatibility';
|
||||||
public const EXTRA_ACTION = 'lit_mcp_compat_profile_extra';
|
public const EXTRA_ACTION = 'lit_mcp_compat_profile_extra';
|
||||||
public const LOAD_ACTION = 'lit_mcp_compat_profile_load';
|
public const LOAD_ACTION = 'lit_mcp_compat_profile_load';
|
||||||
|
public const INTRO_ACTION = 'lit_mcp_compat_profile_intro';
|
||||||
|
public const EXPLANATION_FILTER = 'lit_mcp_compat_explanation';
|
||||||
|
/**
|
||||||
|
* Filter lit_mcp_compat_capability_columns(list, $profileId): a profile adds columns to its capability table.
|
||||||
|
* Each column is array{heading: string, cell: Closure(Capability, CompatibilityMatrix): list<string>,
|
||||||
|
* code?: bool}; the tab escapes the cell items (code: shown as <code>) and prints a dash for none. Columns
|
||||||
|
* without a string heading or a Closure cell are dropped; a cell that throws (logged) or does not return a
|
||||||
|
* list<string> shows a dash. The columns apply to exactly one table: a profile may cache per call (the cell
|
||||||
|
* Closures see the matrix of that table only).
|
||||||
|
*/
|
||||||
|
public const COLUMNS_FILTER = 'lit_mcp_compat_capability_columns';
|
||||||
/** admin-post actions; the nonce action of a request is the action plus "_<profile ID>". */
|
/** admin-post actions; the nonce action of a request is the action plus "_<profile ID>". */
|
||||||
public const RELEASE_ACTION = 'lit_mcp_compat_release';
|
public const RELEASE_ACTION = 'lit_mcp_compat_release';
|
||||||
public const WITHDRAW_ACTION = 'lit_mcp_compat_withdraw';
|
public const WITHDRAW_ACTION = 'lit_mcp_compat_withdraw';
|
||||||
@@ -225,7 +241,7 @@ final class CompatibilityTab implements AdminTab
|
|||||||
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
|
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
|
||||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
||||||
self::stateBadge($state),
|
self::stateBadge($state),
|
||||||
esc_html(self::explanation($state))
|
esc_html($this->explanation($profile, $state))
|
||||||
);
|
);
|
||||||
if ($problems !== []) {
|
if ($problems !== []) {
|
||||||
echo '<tr><th scope="row">' . esc_html__('Probleme', 'wp-lit-mcp') . '</th><td>';
|
echo '<tr><th scope="row">' . esc_html__('Probleme', 'wp-lit-mcp') . '</th><td>';
|
||||||
@@ -249,6 +265,7 @@ final class CompatibilityTab implements AdminTab
|
|||||||
);
|
);
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
|
|
||||||
|
do_action(self::INTRO_ACTION, $profile->id);
|
||||||
$this->renderCapabilities($profile, $matrix);
|
$this->renderCapabilities($profile, $matrix);
|
||||||
$report = ($this->reports)($profile)->latest();
|
$report = ($this->reports)($profile)->latest();
|
||||||
$this->renderForms($profile, $report);
|
$this->renderForms($profile, $report);
|
||||||
@@ -261,8 +278,19 @@ final class CompatibilityTab implements AdminTab
|
|||||||
|
|
||||||
private function renderCapabilities(Profile $profile, CompatibilityMatrix $matrix): void
|
private function renderCapabilities(Profile $profile, CompatibilityMatrix $matrix): void
|
||||||
{
|
{
|
||||||
|
$extra = $this->extraColumns($profile);
|
||||||
$headings = [__('Fähigkeit', 'wp-lit-mcp'), __('Zustand', 'wp-lit-mcp')];
|
$headings = [__('Fähigkeit', 'wp-lit-mcp'), __('Zustand', 'wp-lit-mcp')];
|
||||||
echo '<table class="widefat striped lit-mcp-divi-features"><caption class="screen-reader-text">'
|
foreach ($extra as $column) {
|
||||||
|
$headings[] = $column['heading'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open when something needs attention; a released profile keeps the long table folded.
|
||||||
|
printf(
|
||||||
|
'<details class="lit-mcp-compat-details"%s><summary>%s</summary>',
|
||||||
|
$matrix->state() === ReleaseState::Released ? '' : ' open',
|
||||||
|
esc_html__('Fähigkeiten im Detail', 'wp-lit-mcp')
|
||||||
|
);
|
||||||
|
echo '<table class="widefat striped lit-mcp-compat-features"><caption class="screen-reader-text">'
|
||||||
. esc_html(sprintf(
|
. esc_html(sprintf(
|
||||||
/* translators: %s: plugin name */
|
/* translators: %s: plugin name */
|
||||||
__('Fähigkeiten von %s', 'wp-lit-mcp'),
|
__('Fähigkeiten von %s', 'wp-lit-mcp'),
|
||||||
@@ -276,15 +304,63 @@ final class CompatibilityTab implements AdminTab
|
|||||||
foreach ($profile->capabilities as $capability) {
|
foreach ($profile->capabilities as $capability) {
|
||||||
// data-label: on phones each row becomes a card and the cell names itself.
|
// data-label: on phones each row becomes a card and the cell names itself.
|
||||||
printf(
|
printf(
|
||||||
'<tr><td data-label="%s">%s</td><td data-label="%s">%s</td></tr>',
|
'<tr><td data-label="%s">%s</td><td data-label="%s">%s</td>',
|
||||||
esc_attr($headings[0]),
|
esc_attr($headings[0]),
|
||||||
esc_html($capability->label()),
|
esc_html($capability->label()),
|
||||||
esc_attr($headings[1]),
|
esc_attr($headings[1]),
|
||||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
||||||
self::stateBadge($matrix->of($capability)->state)
|
self::stateBadge($matrix->of($capability)->state)
|
||||||
);
|
);
|
||||||
|
foreach ($extra as $index => $column) {
|
||||||
|
printf('<td data-label="%s">', esc_attr($headings[$index + 2]));
|
||||||
|
self::renderList(
|
||||||
|
self::cellItems($profile, $column, $capability, $matrix),
|
||||||
|
($column['code'] ?? false) === true
|
||||||
|
);
|
||||||
|
echo '</td>';
|
||||||
|
}
|
||||||
|
echo '</tr>';
|
||||||
}
|
}
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table></details>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{heading: string, cell: Closure, code?: bool}> */
|
||||||
|
private function extraColumns(Profile $profile): array
|
||||||
|
{
|
||||||
|
$columns = apply_filters(self::COLUMNS_FILTER, [], $profile->id);
|
||||||
|
|
||||||
|
return is_array($columns)
|
||||||
|
? array_values(array_filter(
|
||||||
|
$columns,
|
||||||
|
static fn (mixed $column): bool => is_array($column)
|
||||||
|
&& is_string($column['heading'] ?? null)
|
||||||
|
&& ($column['cell'] ?? null) instanceof Closure
|
||||||
|
))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What one cell shows: its list, or nothing (a dash) when the profile's callback throws or returns anything but a
|
||||||
|
* list of strings – a third party's column must not break the table.
|
||||||
|
*
|
||||||
|
* @param array{heading: string, cell: Closure} $column
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private static function cellItems(
|
||||||
|
Profile $profile,
|
||||||
|
array $column,
|
||||||
|
Capability $capability,
|
||||||
|
CompatibilityMatrix $matrix
|
||||||
|
): array {
|
||||||
|
try {
|
||||||
|
$items = ($column['cell'])($capability, $matrix);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::failure(sprintf('%s capability column "%s"', $profile->id, $column['heading']), $e);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($items) && array_is_list($items) && array_filter($items, 'is_string') === $items ? $items : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderForms(Profile $profile, ?CheckReport $report): void
|
private function renderForms(Profile $profile, ?CheckReport $report): void
|
||||||
@@ -371,7 +447,7 @@ final class CompatibilityTab implements AdminTab
|
|||||||
private function renderReport(Profile $profile, CheckReport $report): void
|
private function renderReport(Profile $profile, CheckReport $report): void
|
||||||
{
|
{
|
||||||
printf(
|
printf(
|
||||||
'<h3>%s</h3><p>%s</p><ul class="lit-mcp-divi-report">',
|
'<h3>%s</h3><p>%s</p><ul class="lit-mcp-compat-report">',
|
||||||
esc_html__('Letzte Prüfung', 'wp-lit-mcp'),
|
esc_html__('Letzte Prüfung', 'wp-lit-mcp'),
|
||||||
esc_html(sprintf(
|
esc_html(sprintf(
|
||||||
/* translators: 1: plugin version, 2: LIT MCP version, 3: date */
|
/* translators: 1: plugin version, 2: LIT MCP version, 3: date */
|
||||||
@@ -406,11 +482,17 @@ final class CompatibilityTab implements AdminTab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @param list<string> $items */
|
/** @param list<string> $items */
|
||||||
private static function renderList(array $items): void
|
private static function renderList(array $items, bool $code = false): void
|
||||||
{
|
{
|
||||||
|
if ($items === []) {
|
||||||
|
echo '–';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
echo '<ul>';
|
echo '<ul>';
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
echo '<li>' . esc_html($item) . '</li>';
|
echo $code ? '<li><code>' . esc_html($item) . '</code></li>' : '<li>' . esc_html($item) . '</li>';
|
||||||
}
|
}
|
||||||
echo '</ul>';
|
echo '</ul>';
|
||||||
}
|
}
|
||||||
@@ -445,7 +527,15 @@ final class CompatibilityTab implements AdminTab
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function explanation(ReleaseState $state): string
|
private function explanation(Profile $profile, ReleaseState $state): string
|
||||||
|
{
|
||||||
|
$generic = self::genericExplanation($state);
|
||||||
|
$text = apply_filters(self::EXPLANATION_FILTER, $generic, $profile->id, $state);
|
||||||
|
|
||||||
|
return is_string($text) ? $text : $generic;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function genericExplanation(ReleaseState $state): string
|
||||||
{
|
{
|
||||||
return match ($state) {
|
return match ($state) {
|
||||||
ReleaseState::Released => __('Alle Fähigkeiten sind nutzbar.', 'wp-lit-mcp'),
|
ReleaseState::Released => __('Alle Fähigkeiten sind nutzbar.', 'wp-lit-mcp'),
|
||||||
|
|||||||
@@ -266,23 +266,6 @@ final class SettingsPage
|
|||||||
'revoked' => ['success', __('Verbindung widerrufen.', 'wp-lit-mcp')],
|
'revoked' => ['success', __('Verbindung widerrufen.', 'wp-lit-mcp')],
|
||||||
'revoke_failed' => ['error', __('Die Verbindung konnte nicht widerrufen werden.', 'wp-lit-mcp')],
|
'revoke_failed' => ['error', __('Die Verbindung konnte nicht widerrufen werden.', 'wp-lit-mcp')],
|
||||||
'cleared' => ['success', __('Protokoll geleert.', 'wp-lit-mcp')],
|
'cleared' => ['success', __('Protokoll geleert.', 'wp-lit-mcp')],
|
||||||
'divi_released' => ['success', __('Die Divi-Version ist für LIT MCP freigegeben.', 'wp-lit-mcp')],
|
|
||||||
'divi_release_refused' => [
|
|
||||||
'error',
|
|
||||||
__(
|
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
|
||||||
'Freigabe nicht möglich: Der letzte Prüfbericht ist nicht grün oder passt nicht mehr zur installierten Divi- oder Plugin-Version. Bitte die Prüfung erneut ausführen.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
'divi_withdrawn' => [
|
|
||||||
'success',
|
|
||||||
__('Freigabe zurückgezogen; schreibende Divi-Tools sind wieder gesperrt.', 'wp-lit-mcp'),
|
|
||||||
],
|
|
||||||
'divi_withdraw_failed' => [
|
|
||||||
'error',
|
|
||||||
__('Für die installierte Divi-Version gibt es auf dieser Website keine Freigabe.', 'wp-lit-mcp'),
|
|
||||||
],
|
|
||||||
'compat_checked' => ['success', __('Prüfung abgeschlossen; das Ergebnis steht unten.', 'wp-lit-mcp')],
|
'compat_checked' => ['success', __('Prüfung abgeschlossen; das Ergebnis steht unten.', 'wp-lit-mcp')],
|
||||||
'compat_check_running' => [
|
'compat_check_running' => [
|
||||||
'warning',
|
'warning',
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Admin;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use LIT\MCP\Admin\CompatibilityTab;
|
||||||
|
use LIT\MCP\Admin\SettingsPage;
|
||||||
|
use LIT\MCP\Compatibility\Capability;
|
||||||
|
use LIT\MCP\Compatibility\CheckReportStore;
|
||||||
|
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||||
|
use LIT\MCP\Compatibility\FeatureStatus;
|
||||||
|
use LIT\MCP\Compatibility\ReleaseState;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What Divi adds to the tab "Compatibility" (Admin\CompatibilityTab, which owns check, release and withdrawal):
|
||||||
|
* the admin notice on every admin page while a feature is unusable, the removal of check pages a dead check left
|
||||||
|
* behind (in the tab's load action, not while rendering), the columns source / problems / affected tools of the
|
||||||
|
* capability table (E3), the source-specific explanation of the overall state, above the capability table (intro)
|
||||||
|
* the orphan report, the next step and what the check does and, closing the section, the automation note and – with a
|
||||||
|
* report – the Visual Builder hint.
|
||||||
|
*/
|
||||||
|
final class DiviAdminExtras
|
||||||
|
{
|
||||||
|
/** @var list<int> Check pages removeOrphans() deleted in this request; renderIntro() reports them. */
|
||||||
|
private array $removed = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param FeatureOverview|null $overview null = a table without the affected tools.
|
||||||
|
* @param (Closure(): string)|null $version Installed Divi version ('' = none); null = DiviEnvironment.
|
||||||
|
* @param CheckReportStore|null $reports null = the Divi store.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private ?FeatureOverview $overview = null,
|
||||||
|
private ?Closure $version = null,
|
||||||
|
private ?CheckReportStore $reports = null
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
add_action('admin_notices', [$this, 'renderNotice']);
|
||||||
|
add_action(CompatibilityTab::LOAD_ACTION, [$this, 'removeOrphans']);
|
||||||
|
add_action(CompatibilityTab::INTRO_ACTION, [$this, 'renderIntro']);
|
||||||
|
add_action(CompatibilityTab::EXTRA_ACTION, [$this, 'renderExtras']);
|
||||||
|
add_filter(CompatibilityTab::EXPLANATION_FILTER, [$this, 'explanation'], 10, 3);
|
||||||
|
add_filter(CompatibilityTab::COLUMNS_FILTER, [$this, 'columns'], 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only for administrators, only with Divi active, only while a feature is not usable – and not on the tab
|
||||||
|
* "Compatibility", which shows the same table. Reads the cached matrix (D7), so no check runs on an admin page.
|
||||||
|
*/
|
||||||
|
public function renderNotice(): void
|
||||||
|
{
|
||||||
|
if (! current_user_can('manage_options') || $this->version() === '' || $this->onCompatibilityTab()) {
|
||||||
|
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 Tools 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(CompatibilityTab::SLUG)),
|
||||||
|
esc_html__('Details im Reiter „Compatibility“', 'wp-lit-mcp')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function onCompatibilityTab(): bool
|
||||||
|
{
|
||||||
|
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab switch.
|
||||||
|
$tab = is_string($_GET['tab'] ?? null) ? sanitize_key(wp_unslash($_GET['tab'])) : '';
|
||||||
|
|
||||||
|
return is_object($screen) && $screen->id === SettingsPage::SCREEN_HOOK && $tab === CompatibilityTab::SLUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lit_mcp_compat_profile_load('divi'), before any output (the tab fires it for administrators on its own tab):
|
||||||
|
* deletes the check pages a dead check left behind (WriteProbe::removeOrphans(): marked drafts with the check
|
||||||
|
* title, older than ten minutes – no running check uses them). On a GET without nonce on purpose: it removes
|
||||||
|
* only the plugin's own leftovers and is idempotent. renderExtras() only reports what was deleted (renderIntro()).
|
||||||
|
*/
|
||||||
|
public function removeOrphans(string $profileId): void
|
||||||
|
{
|
||||||
|
if ($profileId !== DiviProfile::ID || ! current_user_can('manage_options') || $this->version() === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->removed = WriteProbe::removeOrphans();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Columns of Divi's capability table: where the release comes from, the problems of the capability and the
|
||||||
|
* tools it affects. Reads the overview once per table (the matrix is the same for every row).
|
||||||
|
*
|
||||||
|
* @param list<array<string, mixed>> $columns
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function columns(array $columns, string $profileId): array
|
||||||
|
{
|
||||||
|
if ($profileId !== DiviProfile::ID) {
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = null;
|
||||||
|
$row = function (Capability $capability, CompatibilityMatrix $matrix) use (&$rows): array {
|
||||||
|
$rows ??= array_column(($this->overview ?? FeatureOverview::withoutTools())->rows($matrix), null, 'id');
|
||||||
|
|
||||||
|
return $rows[$capability->id()] ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
...$columns,
|
||||||
|
[
|
||||||
|
'heading' => __('Quelle', 'wp-lit-mcp'),
|
||||||
|
'cell' => fn (Capability $c, CompatibilityMatrix $m): array => array_values(array_filter(
|
||||||
|
[$this->sourceLabel($row($c, $m)['source'] ?? null)]
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'heading' => __('Probleme', 'wp-lit-mcp'),
|
||||||
|
'cell' => static fn (Capability $c, CompatibilityMatrix $m): array => $row($c, $m)['problems'] ?? [],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'heading' => __('Betroffene Tools', 'wp-lit-mcp'),
|
||||||
|
'cell' => static fn (Capability $c, CompatibilityMatrix $m): array => $row($c, $m)['tools'] ?? [],
|
||||||
|
'code' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sourceLabel(?string $source): string
|
||||||
|
{
|
||||||
|
return match ($source) {
|
||||||
|
ReleaseStatus::SOURCE_CODE => __('Im Plugin als getestet hinterlegt', 'wp-lit-mcp'),
|
||||||
|
ReleaseStatus::SOURCE_DATABASE => __('Von dir auf dieser Website freigegeben', 'wp-lit-mcp'),
|
||||||
|
ReleaseStatus::SOURCE_AUTO => __('Automatisch geprüft', 'wp-lit-mcp'),
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lit_mcp_compat_profile_intro('divi'), above the capability table: the deleted check pages, the next step and
|
||||||
|
* what the check does (it writes to the site, so it says so).
|
||||||
|
*/
|
||||||
|
public function renderIntro(string $profileId): void
|
||||||
|
{
|
||||||
|
if ($profileId !== DiviProfile::ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->removed !== []) {
|
||||||
|
printf(
|
||||||
|
'<div class="notice notice-warning inline"><p>%s</p></div>',
|
||||||
|
esc_html(sprintf(
|
||||||
|
/* translators: %s: comma separated page IDs */
|
||||||
|
__('Liegengebliebene Prüfseiten gelöscht: %s', 'wp-lit-mcp'),
|
||||||
|
implode(', ', $this->removed)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renderNextStep(ReleaseGate::status());
|
||||||
|
echo '<p class="description">' . esc_html__(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||||
|
'Die Prüfung legt eine private Testseite an, prüft Lesen und Schreiben und löscht sie wieder.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter lit_mcp_compat_explanation: Divi says where its release comes from (plugin, automatic, this website)
|
||||||
|
* instead of the generic sentence, so the section does not explain the state twice.
|
||||||
|
*/
|
||||||
|
public function explanation(string $text, string $profileId, ReleaseState $state): string
|
||||||
|
{
|
||||||
|
if ($profileId !== DiviProfile::ID) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = ReleaseGate::status();
|
||||||
|
|
||||||
|
return match (true) {
|
||||||
|
$status->state === ReleaseState::Incompatible => __(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||||
|
'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'
|
||||||
|
),
|
||||||
|
$status->state === ReleaseState::Untested => __(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||||
|
'Diese Version ist nicht geprüft. Die Tools der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
$status->source === ReleaseStatus::SOURCE_CODE => __(
|
||||||
|
'Diese Version ist im Plugin als getestet hinterlegt.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
$status->source === ReleaseStatus::SOURCE_AUTO => __(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
||||||
|
'Automatisch freigegeben: Alle Divi-Stellen, die LIT MCP nutzt, stimmen mit der geprüften Referenz überein.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
default => $this->releasedOnThisSite($status),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function releasedOnThisSite(ReleaseStatus $status): string
|
||||||
|
{
|
||||||
|
$user = get_userdata((int) ($status->release['user_id'] ?? 0));
|
||||||
|
$releasedAt = (int) strtotime((string) ($status->release['released_at'] ?? ''));
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
/* translators: 1: date, 2: user name */
|
||||||
|
__('Auf dieser Website freigegeben am %1$s von %2$s.', 'wp-lit-mcp'),
|
||||||
|
date_i18n((string) get_option('date_format'), $releasedAt),
|
||||||
|
is_object($user) && isset($user->display_name) ? (string) $user->display_name : '?'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** lit_mcp_compat_profile_extra('divi'): the end of the Divi section (shown only while Divi is active). */
|
||||||
|
public function renderExtras(string $profileId): void
|
||||||
|
{
|
||||||
|
if ($profileId !== DiviProfile::ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renderAutomationNote();
|
||||||
|
$reports = $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||||
|
if ($reports->latest() !== null) {
|
||||||
|
echo '<p class="description">' . esc_html__(
|
||||||
|
'Optional: eine Divi-Seite im Visual Builder öffnen und speichern.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
) . '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One sentence on what to do now; a released version needs none. */
|
||||||
|
private function renderNextStep(ReleaseStatus $status): void
|
||||||
|
{
|
||||||
|
$text = match ($status->state) {
|
||||||
|
ReleaseState::Untested => sprintf(
|
||||||
|
/* translators: %s: Divi version */
|
||||||
|
__(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
|
||||||
|
'Divi %s ist noch nicht geprüft. Starte die Prüfung; danach kannst du die Version freigeben.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
$status->version
|
||||||
|
),
|
||||||
|
ReleaseState::Incompatible => sprintf(
|
||||||
|
/* translators: %s: Divi version */
|
||||||
|
__(
|
||||||
|
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
|
||||||
|
'Divi %s hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Tools bleiben gesperrt, bis ein Plugin-Update erscheint.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
$status->version
|
||||||
|
),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
if ($text === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<div class="notice notice-%s inline"><p><strong>%s</strong></p></div>',
|
||||||
|
$status->state === ReleaseState::Incompatible ? 'error' : 'warning',
|
||||||
|
esc_html($text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 als ungetestet.',
|
||||||
|
'wp-lit-mcp'
|
||||||
|
),
|
||||||
|
'<code>lit_mcp_divi_auto_release</code>'
|
||||||
|
) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function version(): string
|
||||||
|
{
|
||||||
|
return ($this->version ?? static fn (): string => DiviEnvironment::version() ?? '')();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,583 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\Divi\Admin;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use LIT\MCP\Admin\AdminPostAction;
|
|
||||||
use LIT\MCP\Admin\AdminTab;
|
|
||||||
use LIT\MCP\Admin\CheckRoute;
|
|
||||||
use LIT\MCP\Admin\SettingsPage;
|
|
||||||
use LIT\MCP\ChangeLog\ChangeLog;
|
|
||||||
use LIT\MCP\Compatibility\ReleaseState;
|
|
||||||
use LIT\MCP\Compatibility\CheckReport;
|
|
||||||
use LIT\MCP\Compatibility\CheckReportStore;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
|
||||||
use LIT\MCP\Compatibility\FeatureStatus;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
|
||||||
use LIT\MCP\Compatibility\ReleaseStore;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
|
||||||
use LIT\MCP\Plugin;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, Admin\CheckRoute), "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
|
|
||||||
{
|
|
||||||
public const SLUG = 'divi';
|
|
||||||
public const RELEASE_ACTION = 'lit_mcp_divi_release';
|
|
||||||
public const WITHDRAW_ACTION = 'lit_mcp_divi_withdraw';
|
|
||||||
|
|
||||||
/** @var list<int> Check pages removeOrphans() deleted in this request; render() reports them. */
|
|
||||||
private array $removed = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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,
|
|
||||||
private ?ReleaseStore $releases = null,
|
|
||||||
private ?ChangeLog $log = null,
|
|
||||||
private ?AdminPostAction $action = null,
|
|
||||||
private ?Closure $version = null,
|
|
||||||
private ?Closure $stateKey = null,
|
|
||||||
private ?FeatureOverview $overview = null
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function slug(): string
|
|
||||||
{
|
|
||||||
return self::SLUG;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function label(): string
|
|
||||||
{
|
|
||||||
return __('Divi', 'wp-lit-mcp');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function registerSettings(): void
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function registerActions(): void
|
|
||||||
{
|
|
||||||
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 Tools 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* load-<page> of the settings page, before any output: on this tab, for manage_options and with Divi active,
|
|
||||||
* deletes the check pages a dead check left behind (WriteProbe::removeOrphans(): marked drafts with the check
|
|
||||||
* title, older than ten minutes – no running check uses them). On a GET without nonce on purpose: it removes
|
|
||||||
* only the plugin's own leftovers and is idempotent. render() only reports what was deleted.
|
|
||||||
*/
|
|
||||||
public function removeOrphans(): void
|
|
||||||
{
|
|
||||||
if (! $this->requested() || ! current_user_can('manage_options') || $this->version() === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->removed = WriteProbe::removeOrphans();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function release(): void
|
|
||||||
{
|
|
||||||
$action = $this->action ?? new AdminPostAction();
|
|
||||||
$action->guard();
|
|
||||||
check_admin_referer(self::RELEASE_ACTION);
|
|
||||||
|
|
||||||
$version = $this->version();
|
|
||||||
$report = $this->reports()->latest();
|
|
||||||
$fresh = $report !== null
|
|
||||||
&& $report->freshFor($version, Plugin::VERSION, $this->stateKey($version));
|
|
||||||
if ($version === '' || ! $fresh) {
|
|
||||||
$action->redirect(self::SLUG, 'divi_release_refused');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->releases()->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
|
|
||||||
ReleaseGate::invalidate();
|
|
||||||
$this->log?->record(
|
|
||||||
'lit/divi-release',
|
|
||||||
'divi',
|
|
||||||
$version,
|
|
||||||
sprintf('Divi %s für LIT MCP %s freigegeben (Prüfbericht %s).', $version, Plugin::VERSION, $report->id)
|
|
||||||
);
|
|
||||||
$action->redirect(self::SLUG, 'divi_released');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function withdraw(): void
|
|
||||||
{
|
|
||||||
$action = $this->action ?? new AdminPostAction();
|
|
||||||
$action->guard();
|
|
||||||
check_admin_referer(self::WITHDRAW_ACTION);
|
|
||||||
|
|
||||||
$version = $this->version();
|
|
||||||
if (! $this->releases()->withdraw($version, Plugin::VERSION)) {
|
|
||||||
$action->redirect(self::SLUG, 'divi_withdraw_failed');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReleaseGate::invalidate();
|
|
||||||
$this->log?->record(
|
|
||||||
'lit/divi-release',
|
|
||||||
'divi',
|
|
||||||
$version,
|
|
||||||
sprintf('Freigabe von Divi %s zurückgezogen.', $version)
|
|
||||||
);
|
|
||||||
$action->redirect(self::SLUG, 'divi_withdrawn');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): void
|
|
||||||
{
|
|
||||||
if ($this->version() === '') {
|
|
||||||
echo '<p>' . esc_html__(
|
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
|
|
||||||
'Divi ist auf dieser Website nicht aktiv; die Divi-Freigabe ist ohne Bedeutung.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
) . '</p>';
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = ReleaseGate::status();
|
|
||||||
$report = $this->reports()->latest();
|
|
||||||
|
|
||||||
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 prüft nach einem Divi-Update jede Fähigkeit einzeln (z. B. „Seiten schreiben“). Ist sie nicht nutzbar, lehnen nur die Tools ab, die sie brauchen; alle anderen laufen weiter.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
) . '</p>';
|
|
||||||
if ($this->removed !== []) {
|
|
||||||
printf(
|
|
||||||
'<div class="notice notice-warning inline"><p>%s</p></div>',
|
|
||||||
esc_html(sprintf(
|
|
||||||
/* translators: %s: comma separated page IDs */
|
|
||||||
__('Liegengebliebene Prüfseiten gelöscht: %s', 'wp-lit-mcp'),
|
|
||||||
implode(', ', $this->removed)
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->renderNextStep($status);
|
|
||||||
$this->renderStatus($status);
|
|
||||||
$this->renderFeatures($status);
|
|
||||||
$this->renderForms($status, $report);
|
|
||||||
if ($report !== null) {
|
|
||||||
$this->renderReport($report);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One sentence on what to do now, above the details; a released version needs none. */
|
|
||||||
private function renderNextStep(ReleaseStatus $status): void
|
|
||||||
{
|
|
||||||
if ($status->version === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$text = match ($status->state) {
|
|
||||||
ReleaseState::Untested => sprintf(
|
|
||||||
/* translators: %s: Divi version */
|
|
||||||
__(
|
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
|
|
||||||
'Divi %s ist noch nicht geprüft. Starte unten die Prüfung; danach kannst du die Version freigeben.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
),
|
|
||||||
$status->version
|
|
||||||
),
|
|
||||||
ReleaseState::Incompatible => sprintf(
|
|
||||||
/* translators: %s: Divi version */
|
|
||||||
__(
|
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
|
|
||||||
'Divi %s hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Tools bleiben gesperrt, bis ein Plugin-Update erscheint.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
),
|
|
||||||
$status->version
|
|
||||||
),
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
if ($text === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf(
|
|
||||||
'<div class="notice notice-%s inline"><p><strong>%s</strong></p></div>',
|
|
||||||
$status->state === ReleaseState::Incompatible ? 'error' : 'warning',
|
|
||||||
esc_html($text)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderStatus(ReleaseStatus $status): void
|
|
||||||
{
|
|
||||||
echo '<table class="form-table" role="presentation"><tbody>';
|
|
||||||
printf(
|
|
||||||
'<tr><th scope="row">%s</th><td>%s</td></tr>',
|
|
||||||
esc_html__('Divi-Version', 'wp-lit-mcp'),
|
|
||||||
esc_html($status->version === '' ? __('nicht erkannt', 'wp-lit-mcp') : $status->version)
|
|
||||||
);
|
|
||||||
printf(
|
|
||||||
'<tr><th scope="row">%s</th><td>%s – %s</td></tr>',
|
|
||||||
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
|
|
||||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
|
||||||
$this->stateBadge($status->state),
|
|
||||||
esc_html($this->explanation($status))
|
|
||||||
);
|
|
||||||
if ($status->problems !== []) {
|
|
||||||
echo '<tr><th scope="row">' . esc_html__('Geändert in Divi', 'wp-lit-mcp') . '</th><td><ul>';
|
|
||||||
foreach ($status->problems as $problem) {
|
|
||||||
echo '<li>' . esc_html($problem) . '</li>';
|
|
||||||
}
|
|
||||||
echo '</ul></td></tr>';
|
|
||||||
}
|
|
||||||
echo '</tbody></table>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderFeatures(ReleaseStatus $status): void
|
|
||||||
{
|
|
||||||
// Open when something needs attention; a released version keeps the long table folded.
|
|
||||||
printf(
|
|
||||||
'<details class="lit-mcp-divi-details"%s><summary>%s</summary>',
|
|
||||||
$status->state === ReleaseState::Released ? '' : ' open',
|
|
||||||
esc_html__('Fähigkeiten im Detail', 'wp-lit-mcp')
|
|
||||||
);
|
|
||||||
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 Tools', '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) {
|
|
||||||
// data-label: on phones each row becomes a card and the cell names itself.
|
|
||||||
printf('<tr><td data-label="%s">%s</td>', esc_attr($headings[0]), esc_html($row['label']));
|
|
||||||
printf(
|
|
||||||
'<td data-label="%s">%s</td>',
|
|
||||||
esc_attr($headings[1]),
|
|
||||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
|
||||||
$this->stateBadge(ReleaseState::from($row['state']))
|
|
||||||
);
|
|
||||||
printf(
|
|
||||||
'<td data-label="%s">%s</td>',
|
|
||||||
esc_attr($headings[2]),
|
|
||||||
esc_html($this->sourceLabel($row['source']))
|
|
||||||
);
|
|
||||||
printf('<td data-label="%s">', esc_attr($headings[3]));
|
|
||||||
$this->renderList($row['problems'], false);
|
|
||||||
printf('</td><td data-label="%s">', esc_attr($headings[4]));
|
|
||||||
$this->renderList($row['tools'], true);
|
|
||||||
echo '</td></tr>';
|
|
||||||
}
|
|
||||||
echo '</tbody></table>';
|
|
||||||
$this->renderAutomationNote();
|
|
||||||
echo '</details>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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 => __('Im Plugin als getestet hinterlegt', 'wp-lit-mcp'),
|
|
||||||
ReleaseStatus::SOURCE_DATABASE => __('Von dir auf dieser Website freigegeben', 'wp-lit-mcp'),
|
|
||||||
ReleaseStatus::SOURCE_AUTO => __('Automatisch geprüft', '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>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same coloured icons as the status list on the tab "Verbindung"; the word stays, colour is never alone. */
|
|
||||||
private function stateBadge(ReleaseState $state): string
|
|
||||||
{
|
|
||||||
$icon = match ($state) {
|
|
||||||
ReleaseState::Released => 'yes-alt',
|
|
||||||
ReleaseState::Untested => 'warning',
|
|
||||||
ReleaseState::Incompatible => 'dismiss',
|
|
||||||
};
|
|
||||||
|
|
||||||
return sprintf(
|
|
||||||
'<span class="lit-mcp-state lit-mcp-status-%1$s"><span class="dashicons dashicons-%1$s"'
|
|
||||||
. ' aria-hidden="true"></span> <strong>%2$s</strong></span>',
|
|
||||||
$icon,
|
|
||||||
esc_html($this->stateLabel($state))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function stateLabel(ReleaseState $state): string
|
|
||||||
{
|
|
||||||
return match ($state) {
|
|
||||||
ReleaseState::Released => __('freigegeben', 'wp-lit-mcp'),
|
|
||||||
ReleaseState::Untested => __('ungetestet', 'wp-lit-mcp'),
|
|
||||||
ReleaseState::Incompatible => __('inkompatibel', 'wp-lit-mcp'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function explanation(ReleaseStatus $status): string
|
|
||||||
{
|
|
||||||
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. 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 __(
|
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
|
||||||
'Diese Version ist nicht geprüft. Die Tools der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ($status->source === ReleaseStatus::SOURCE_CODE) {
|
|
||||||
return __('Diese Version ist im Plugin als getestet hinterlegt.', 'wp-lit-mcp');
|
|
||||||
}
|
|
||||||
if ($status->source === ReleaseStatus::SOURCE_AUTO) {
|
|
||||||
return __(
|
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
|
||||||
'Automatisch freigegeben: Alle Divi-Stellen, die LIT MCP nutzt, stimmen mit der geprüften Referenz überein.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = get_userdata((int) ($status->release['user_id'] ?? 0));
|
|
||||||
$releasedAt = (int) strtotime((string) ($status->release['released_at'] ?? ''));
|
|
||||||
|
|
||||||
return sprintf(
|
|
||||||
/* translators: 1: date, 2: user name */
|
|
||||||
__('Auf dieser Website freigegeben am %1$s von %2$s.', 'wp-lit-mcp'),
|
|
||||||
date_i18n((string) get_option('date_format'), $releasedAt),
|
|
||||||
is_object($user) && isset($user->display_name) ? (string) $user->display_name : '?'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderForms(ReleaseStatus $status, ?CheckReport $report): void
|
|
||||||
{
|
|
||||||
$fresh = $report !== null
|
|
||||||
&& $report->freshFor($status->version, Plugin::VERSION, $this->stateKey($status->version));
|
|
||||||
$untested = $status->state === ReleaseState::Untested && $status->version !== '';
|
|
||||||
|
|
||||||
printf('<form method="post" action="%s">', esc_url(CheckRoute::url(DiviProfile::ID)));
|
|
||||||
printf('<input type="hidden" name="_wpnonce" value="%s">', esc_attr(wp_create_nonce('wp_rest')));
|
|
||||||
// The check is the one thing to do while untested without a usable report; otherwise the release leads.
|
|
||||||
submit_button(
|
|
||||||
__('Prüfung starten', 'wp-lit-mcp'),
|
|
||||||
$untested && ! $fresh ? 'primary' : 'secondary',
|
|
||||||
'lit-mcp-divi-check',
|
|
||||||
false
|
|
||||||
);
|
|
||||||
echo '<p class="description">' . esc_html__(
|
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
|
||||||
'Die Prüfung legt eine private Testseite an, prüft Lesen und Schreiben und löscht sie wieder. Sie dauert einige Sekunden.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
) . '</p>';
|
|
||||||
echo '</form>';
|
|
||||||
|
|
||||||
if ($untested) {
|
|
||||||
if ($fresh) {
|
|
||||||
$this->actionForm(
|
|
||||||
self::RELEASE_ACTION,
|
|
||||||
/* translators: %s: Divi version */
|
|
||||||
sprintf(__('Divi %s freigeben', 'wp-lit-mcp'), $status->version),
|
|
||||||
'primary'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
echo '<p>' . esc_html__(
|
|
||||||
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
|
|
||||||
'Freigeben geht erst nach einer vollständig grünen Prüfung dieser Divi-Version – Prüfung erneut ausführen.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
) . '</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function actionForm(string $action, string $label, string $type): void
|
|
||||||
{
|
|
||||||
printf('<form method="post" action="%s">', esc_url(admin_url('admin-post.php')));
|
|
||||||
printf('<input type="hidden" name="action" value="%s">', esc_attr($action));
|
|
||||||
wp_nonce_field($action);
|
|
||||||
submit_button($label, $type, 'submit', false);
|
|
||||||
echo '</form>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderReport(CheckReport $report): void
|
|
||||||
{
|
|
||||||
printf(
|
|
||||||
'<h3>%s</h3><p>%s</p><ul class="lit-mcp-divi-report">',
|
|
||||||
esc_html__('Letzte Prüfung', 'wp-lit-mcp'),
|
|
||||||
esc_html(sprintf(
|
|
||||||
/* translators: 1: Divi version, 2: plugin version, 3: date */
|
|
||||||
__('Divi %1$s mit LIT MCP %2$s, %3$s', 'wp-lit-mcp'),
|
|
||||||
$report->version,
|
|
||||||
$report->pluginVersion,
|
|
||||||
date_i18n((string) get_option('date_format'), (int) strtotime($report->createdAt))
|
|
||||||
))
|
|
||||||
);
|
|
||||||
foreach ($report->steps as $step) {
|
|
||||||
[$icon, $word] = match (true) {
|
|
||||||
$step->skipped => ['minus', __('übersprungen', 'wp-lit-mcp')],
|
|
||||||
$step->green() => ['yes-alt', __('bestanden', 'wp-lit-mcp')],
|
|
||||||
default => ['dismiss', __('fehlgeschlagen', 'wp-lit-mcp')],
|
|
||||||
};
|
|
||||||
printf(
|
|
||||||
'<li class="lit-mcp-state lit-mcp-status-%1$s"><span class="dashicons dashicons-%1$s"'
|
|
||||||
. ' aria-hidden="true"></span><span class="screen-reader-text">%2$s:</span> %3$s',
|
|
||||||
esc_attr($icon),
|
|
||||||
esc_html($word),
|
|
||||||
esc_html(DiviCheckSteps::label($step))
|
|
||||||
);
|
|
||||||
if ($step->skipped) {
|
|
||||||
echo ' (' . esc_html__('nicht ausgeführt', 'wp-lit-mcp') . ')';
|
|
||||||
}
|
|
||||||
if ($step->problems !== []) {
|
|
||||||
echo '<ul>';
|
|
||||||
foreach ($step->problems as $problem) {
|
|
||||||
echo '<li>' . esc_html($problem) . '</li>';
|
|
||||||
}
|
|
||||||
echo '</ul>';
|
|
||||||
}
|
|
||||||
echo '</li>';
|
|
||||||
}
|
|
||||||
echo '</ul><p class="description">' . esc_html__(
|
|
||||||
'Optional: eine Divi-Seite im Visual Builder öffnen und speichern.',
|
|
||||||
'wp-lit-mcp'
|
|
||||||
) . '</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whether this request opens this tab of the settings page (the load hook fires for every tab). */
|
|
||||||
private function requested(): bool
|
|
||||||
{
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab switch.
|
|
||||||
$tab = isset($_GET['tab']) ? sanitize_key(wp_unslash((string) $_GET['tab'])) : '';
|
|
||||||
|
|
||||||
return $tab === self::SLUG;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function version(): string
|
|
||||||
{
|
|
||||||
return ($this->version ?? static fn (): string => DiviEnvironment::version() ?? '')();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function stateKey(string $version): string
|
|
||||||
{
|
|
||||||
return ($this->stateKey ?? static fn (string $v): string => ReleaseResolver::stateKey($v))($version);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function releases(): ReleaseStore
|
|
||||||
{
|
|
||||||
return $this->releases ?? new ReleaseStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function reports(): CheckReportStore
|
|
||||||
{
|
|
||||||
return $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -72,8 +72,8 @@ final class DiviCheckCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft die installierte Divi-Version wie der Reiter „Divi“: Manifeste, Fingerabdrücke, Modulkatalog, Format
|
* Prüft die installierte Divi-Version wie der Reiter „Compatibility“: Manifeste, Fingerabdrücke, Modulkatalog,
|
||||||
* und Schreibtest auf einer Wegwerfseite. Gibt danach den Zustand je Fähigkeit aus und gibt nichts frei.
|
* Format und Schreibtest auf einer Wegwerfseite. Gibt danach den Zustand je Fähigkeit aus und gibt nichts frei.
|
||||||
* Exit-Code 0: Prüfung grün und alle Fähigkeiten nutzbar; 1: Prüfung rot; 2: Prüfung grün, aber mindestens
|
* Exit-Code 0: Prüfung grün und alle Fähigkeiten nutzbar; 1: Prüfung rot; 2: Prüfung grün, aber mindestens
|
||||||
* eine Fähigkeit nicht nutzbar (z. B. Version noch nicht in DiviReleases::TESTED).
|
* eine Fähigkeit nicht nutzbar (z. B. Version noch nicht in DiviReleases::TESTED).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ use Closure;
|
|||||||
use LIT\MCP\Abilities\Ability;
|
use LIT\MCP\Abilities\Ability;
|
||||||
use LIT\MCP\Abilities\ToolAvailability;
|
use LIT\MCP\Abilities\ToolAvailability;
|
||||||
use LIT\MCP\Admin\CheckRoute;
|
use LIT\MCP\Admin\CheckRoute;
|
||||||
use LIT\MCP\Admin\SettingsPage;
|
|
||||||
use LIT\MCP\ChangeLog\ChangeLog;
|
use LIT\MCP\ChangeLog\ChangeLog;
|
||||||
use LIT\MCP\Compatibility\CompatibilityCheck;
|
use LIT\MCP\Compatibility\CompatibilityCheck;
|
||||||
use LIT\MCP\Compatibility\Profile;
|
use LIT\MCP\Compatibility\Profile;
|
||||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||||
use LIT\MCP\Integrations\Bootable;
|
use LIT\MCP\Integrations\Bootable;
|
||||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
use LIT\MCP\Integrations\Divi\Admin\DiviAdminExtras;
|
||||||
use LIT\MCP\Integrations\Divi\Cli\DiviCheckCommand;
|
use LIT\MCP\Integrations\Divi\Cli\DiviCheckCommand;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||||
use LIT\MCP\Integrations\Divi\Design\ColorRestorePlanner;
|
use LIT\MCP\Integrations\Divi\Design\ColorRestorePlanner;
|
||||||
@@ -227,7 +226,7 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
|||||||
return [new DiviLogoUsage()];
|
return [new DiviLogoUsage()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Which tools each Divi feature affects – for lit-site-status and the Divi tab (E3). */
|
/** Which tools each Divi feature affects – for lit-site-status and the tab "Compatibility" (E3). */
|
||||||
public function featureOverview(): FeatureOverview
|
public function featureOverview(): FeatureOverview
|
||||||
{
|
{
|
||||||
return new FeatureOverview($this->allAbilities ?? static fn (): array => []);
|
return new FeatureOverview($this->allAbilities ?? static fn (): array => []);
|
||||||
@@ -250,8 +249,8 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Divi's own boot steps, registered whether or not Divi is loaded yet (Bootable): a theme update checks the
|
* Divi's own boot steps, registered whether or not Divi is loaded yet (Bootable): a theme update checks the
|
||||||
* fingerprints again at once (spec §5.4), the admin check as REST route and WP-CLI command (spec §6, §7) and
|
* fingerprints again at once (spec §5.4), the WP-CLI command (spec §7) and the Divi additions to the admin tab
|
||||||
* the tab "Divi" after the plugin's own tabs.
|
* "Compatibility" (spec §6; the check route itself is generic, Admin\CheckRoute).
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
@@ -273,10 +272,8 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
|
|||||||
$profile = $this->profile();
|
$profile = $this->profile();
|
||||||
DiviCheckCommand::register(static fn (): CompatibilityCheck => CompatibilityCheck::forProfile($profile));
|
DiviCheckCommand::register(static fn (): CompatibilityCheck => CompatibilityCheck::forProfile($profile));
|
||||||
|
|
||||||
add_filter(
|
// No tab of its own: notice, orphan cleanup, columns and notes join the tab "Compatibility".
|
||||||
SettingsPage::TABS_FILTER,
|
(new DiviAdminExtras($this->featureOverview()))->register();
|
||||||
fn (array $tabs): array => [...$tabs, new DiviTab(log: $this->log, overview: $this->featureOverview())]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function abilities(): array
|
public function abilities(): array
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Divi versions checked with this plugin version (spec §5.3, source "code"): add a version after
|
* Divi versions checked with this plugin version (spec §5.3, source "code"): add a version after
|
||||||
* wp lit-mcp divi-check ran green against it. Any other version needs a release in the Divi tab of each site.
|
* wp lit-mcp divi-check ran green against it. Any other version needs a release in the tab "Compatibility" of each
|
||||||
|
* site.
|
||||||
*/
|
*/
|
||||||
final class DiviReleases
|
final class DiviReleases
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class FeatureOverview
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An overview that knows no tools (tests, and a Divi tab built without a registry). */
|
/** An overview that knows no tools (tests, and an admin tab built without a registry). */
|
||||||
public static function withoutTools(): self
|
public static function withoutTools(): self
|
||||||
{
|
{
|
||||||
return new self(static fn (): array => []);
|
return new self(static fn (): array => []);
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ final class ReleaseStatus
|
|||||||
public const SOURCE_AUTO = CompatibilityMatrix::SOURCE_AUTO;
|
public const SOURCE_AUTO = CompatibilityMatrix::SOURCE_AUTO;
|
||||||
|
|
||||||
private const FEATURE_UNTESTED = 'Achtung: %1$s ist auf dieser Website für %2$s noch nicht für LIT MCP '
|
private const FEATURE_UNTESTED = 'Achtung: %1$s ist auf dieser Website für %2$s noch nicht für LIT MCP '
|
||||||
. 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung '
|
. 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility die '
|
||||||
. 'ausführt und die Version freigibt. Es wurde nichts geändert.';
|
. 'Prüfung ausführt und die Version freigibt. Es wurde nichts geändert.';
|
||||||
private const FEATURE_UNTESTED_HINT = 'Gesperrt: %s. Andere Divi-Werkzeuge funktionieren weiter, sofern ihre '
|
private const FEATURE_UNTESTED_HINT = 'Gesperrt: %s. Andere Divi-Werkzeuge funktionieren weiter, sofern ihre '
|
||||||
. 'Fähigkeiten freigegeben sind. Den Nutzer bitten, die Divi-Prüfung auszuführen und die Version freizugeben.';
|
. 'Fähigkeiten freigegeben sind. Den Nutzer bitten, die Divi-Prüfung auszuführen und die Version freizugeben.';
|
||||||
private const FEATURE_INCOMPATIBLE = '%1$s hat Stellen geändert, die LIT MCP für %2$s nutzt. Das ist gesperrt, '
|
private const FEATURE_INCOMPATIBLE = '%1$s hat Stellen geändert, die LIT MCP für %2$s nutzt. Das ist gesperrt, '
|
||||||
|
|||||||
+2
-2
@@ -176,8 +176,8 @@ final class Plugin
|
|||||||
Limits $limits,
|
Limits $limits,
|
||||||
ToolAvailability $tools
|
ToolAvailability $tools
|
||||||
): IntegrationRegistry {
|
): IntegrationRegistry {
|
||||||
// E3: lit-site-status and the Divi tab name the tools each feature affects; the registry exists only after
|
// E3: lit-site-status and the tab "Compatibility" name the tools each feature affects; the registry exists
|
||||||
// the Divi integration, so it is asked at call time.
|
// only after the Divi integration, so it is asked at call time.
|
||||||
$registry = null;
|
$registry = null;
|
||||||
$allAbilities = static function () use (&$registry): array {
|
$allAbilities = static function () use (&$registry): array {
|
||||||
return $registry instanceof IntegrationRegistry ? $registry->abilities() : [];
|
return $registry instanceof IntegrationRegistry ? $registry->abilities() : [];
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ pass "lit-site-status: sixteen features released (code), each with its tools"
|
|||||||
APP_PASSWORD=$(wp user application-password create "$ADMIN_LOGIN" lit-mcp-integration-test --porcelain)
|
APP_PASSWORD=$(wp user application-password create "$ADMIN_LOGIN" lit-mcp-integration-test --porcelain)
|
||||||
HEADERS=$(curl -s -o /dev/null -D - -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' -u "$ADMIN_LOGIN:$APP_PASSWORD" "$BASE/wp-json/lit-mcp/v1/divi-check")
|
HEADERS=$(curl -s -o /dev/null -D - -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' -u "$ADMIN_LOGIN:$APP_PASSWORD" "$BASE/wp-json/lit-mcp/v1/divi-check")
|
||||||
echo "$HEADERS" | grep -qi '^HTTP/[0-9.]* 303' || fail "check route must answer 303: $HEADERS"
|
echo "$HEADERS" | grep -qi '^HTTP/[0-9.]* 303' || fail "check route must answer 303: $HEADERS"
|
||||||
echo "$HEADERS" | grep -qi '^location:.*tab=divi.*lit_mcp_notice=divi_checked' || fail "redirect to the tab: $HEADERS"
|
echo "$HEADERS" | grep -qi '^location:.*tab=compatibility.*lit_mcp_notice=compat_checked' || fail "redirect to the tab: $HEADERS"
|
||||||
REPORT=$(wp option get lit_mcp_divi_check_report --format=json)
|
REPORT=$(wp option get lit_mcp_divi_check_report --format=json)
|
||||||
echo "$REPORT" | jq -e '[.steps[] | (.problems == [] and .skipped == false)] | all and length == 5' >/dev/null \
|
echo "$REPORT" | jq -e '[.steps[] | (.problems == [] and .skipped == false)] | all and length == 5' >/dev/null \
|
||||||
|| fail "every check step must be green: $REPORT"
|
|| fail "every check step must be green: $REPORT"
|
||||||
@@ -162,31 +162,21 @@ echo (int) $wpdb->get_var($wpdb->prepare(
|
|||||||
pass "site status: only page-write incompatible (problem and tools named), refusal logged"
|
pass "site status: only page-write incompatible (problem and tools named), refusal logged"
|
||||||
|
|
||||||
# The admin sees the same table and a notice with a link to the tab (rendered here without a browser).
|
# The admin sees the same table and a notice with a link to the tab (rendered here without a browser).
|
||||||
# The tab is taken from the settings page filter, as the plugin wires it (with the tools of every integration).
|
# The notice is the Divi admin extra; the tab is the generic one over the Divi profile (the plugin wires the same
|
||||||
NOTICE=$(wp --user="$ADMIN_LOGIN" eval '$tab = null;
|
# hooks: Divi's columns and notes come through lit_mcp_compat_capability_columns / lit_mcp_compat_profile_extra).
|
||||||
foreach (apply_filters(LIT\MCP\Admin\SettingsPage::TABS_FILTER, []) as $candidate) {
|
NOTICE=$(wp --user="$ADMIN_LOGIN" eval '(new LIT\MCP\Integrations\Divi\Admin\DiviAdminExtras())->renderNotice();')
|
||||||
if ($candidate instanceof LIT\MCP\Integrations\Divi\Admin\DiviTab) {
|
|
||||||
$tab = $candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$tab ?? exit(1);
|
|
||||||
$tab->renderNotice();')
|
|
||||||
echo "$NOTICE" | grep -q 'Seiten schreiben' || fail "admin notice must name the feature: $NOTICE"
|
echo "$NOTICE" | grep -q 'Seiten schreiben' || fail "admin notice must name the feature: $NOTICE"
|
||||||
echo "$NOTICE" | grep -q 'tab=divi' || fail "admin notice must link to the Divi tab: $NOTICE"
|
echo "$NOTICE" | grep -q 'tab=compatibility' || fail "admin notice must link to the tab Compatibility: $NOTICE"
|
||||||
TAB=$(wp --user="$ADMIN_LOGIN" eval '
|
TAB=$(wp --user="$ADMIN_LOGIN" eval '
|
||||||
require_once ABSPATH . "wp-admin/includes/template.php";
|
require_once ABSPATH . "wp-admin/includes/template.php";
|
||||||
$tab = null;
|
(new LIT\MCP\Admin\CompatibilityTab(
|
||||||
foreach (apply_filters(LIT\MCP\Admin\SettingsPage::TABS_FILTER, []) as $candidate) {
|
new LIT\MCP\Compatibility\Compatibility([LIT\MCP\Integrations\Divi\Release\DiviProfile::profile()])
|
||||||
if ($candidate instanceof LIT\MCP\Integrations\Divi\Admin\DiviTab) {
|
))->render();')
|
||||||
$tab = $candidate;
|
echo "$TAB" | grep -q 'lit-mcp-compat-features' || fail "tab Compatibility must list the features: $TAB"
|
||||||
}
|
echo "$TAB" | grep -q 'lit-divi-insert' || fail "tab Compatibility must name the affected tools: $TAB"
|
||||||
}
|
echo "$TAB" | grep -q 'Smoke: Seiten schreiben geändert' || fail "tab Compatibility must show the problem: $TAB"
|
||||||
$tab ?? exit(1);
|
echo "$TAB" | grep -q 'lit_mcp_divi_auto_release' || fail "tab Compatibility must explain the filter: $TAB"
|
||||||
$tab->render();')
|
pass "admin notice and tab Compatibility show the incompatible feature with its tools"
|
||||||
echo "$TAB" | grep -q 'lit-mcp-divi-features' || fail "Divi tab must list the features: $TAB"
|
|
||||||
echo "$TAB" | grep -q 'lit-divi-insert' || fail "Divi tab must name the affected tools: $TAB"
|
|
||||||
echo "$TAB" | grep -q 'lit_mcp_divi_auto_release' || fail "Divi tab must explain the filter: $TAB"
|
|
||||||
pass "admin notice and Divi tab show the incompatible feature with its tools"
|
|
||||||
|
|
||||||
# 4. Back to released: writes work again
|
# 4. Back to released: writes work again
|
||||||
wp transient delete lit_mcp_divi_state >/dev/null
|
wp transient delete lit_mcp_divi_state >/dev/null
|
||||||
@@ -196,14 +186,7 @@ echo "$INSERT" | jq -e '.ok == true' >/dev/null || fail "write must work when re
|
|||||||
STATUS=$(tool lit-site-status '{}')
|
STATUS=$(tool lit-site-status '{}')
|
||||||
echo "$STATUS" | jq -e '.components[] | select(.id == "divi") | .features | all(.state == "released")' >/dev/null \
|
echo "$STATUS" | jq -e '.components[] | select(.id == "divi") | .features | all(.state == "released")' >/dev/null \
|
||||||
|| fail "every feature must be released again: $STATUS"
|
|| fail "every feature must be released again: $STATUS"
|
||||||
[ -z "$(wp --user="$ADMIN_LOGIN" eval '$tab = null;
|
[ -z "$(wp --user="$ADMIN_LOGIN" eval '(new LIT\MCP\Integrations\Divi\Admin\DiviAdminExtras())->renderNotice();')" ] \
|
||||||
foreach (apply_filters(LIT\MCP\Admin\SettingsPage::TABS_FILTER, []) as $candidate) {
|
|
||||||
if ($candidate instanceof LIT\MCP\Integrations\Divi\Admin\DiviTab) {
|
|
||||||
$tab = $candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$tab ?? exit(1);
|
|
||||||
$tab->renderNotice();')" ] \
|
|
||||||
|| fail "no admin notice while every feature is usable"
|
|| fail "no admin notice while every feature is usable"
|
||||||
pass "released again: writing works, matrix green, no admin notice"
|
pass "released again: writing works, matrix green, no admin notice"
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Tests\Unit\Admin;
|
namespace LIT\MCP\Tests\Unit\Admin;
|
||||||
|
|
||||||
use Brain\Monkey\Actions;
|
use Brain\Monkey\Actions;
|
||||||
|
use Brain\Monkey\Filters;
|
||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\Admin\AdminPostAction;
|
use LIT\MCP\Admin\AdminPostAction;
|
||||||
@@ -312,6 +313,45 @@ final class CompatibilityTabTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAProfileCanAddColumnsToTheCapabilityTableAndTheTabEscapesThem(): void
|
||||||
|
{
|
||||||
|
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->with([], 'fake')->andReturn([
|
||||||
|
[
|
||||||
|
'heading' => 'Quelle <b>',
|
||||||
|
'cell' => static fn (FakeCapability $c, CompatibilityMatrix $m): array => $c === FakeCapability::Read
|
||||||
|
? ['<i>a</i>']
|
||||||
|
: [],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'heading' => 'Tools',
|
||||||
|
'cell' => static fn (FakeCapability $c, CompatibilityMatrix $m): array => ['tool-' . $c->id()],
|
||||||
|
'code' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = self::html($this->tab(self::profile()));
|
||||||
|
|
||||||
|
self::assertStringContainsString('<th scope="col">Quelle <b></th>', $html);
|
||||||
|
self::assertStringContainsString('<td data-label="Quelle <b>"><ul><li><i>a</i></li></ul></td>', $html);
|
||||||
|
self::assertStringContainsString('<td data-label="Quelle <b>">–</td>', $html);
|
||||||
|
self::assertStringContainsString('<li><code>tool-' . FakeCapability::Read->id() . '</code></li>', $html);
|
||||||
|
self::assertStringNotContainsString('<i>a</i>', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheCapabilityTableIsOpenUnlessReleasedAndFolded(): void
|
||||||
|
{
|
||||||
|
$released = self::html($this->tab(
|
||||||
|
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released))
|
||||||
|
));
|
||||||
|
$untested = self::html($this->tab(
|
||||||
|
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested))
|
||||||
|
));
|
||||||
|
|
||||||
|
self::assertStringContainsString('<details class="lit-mcp-compat-details">', $released);
|
||||||
|
self::assertStringContainsString('<details class="lit-mcp-compat-details" open>', $untested);
|
||||||
|
self::assertStringContainsString('lit-mcp-compat-features', $released);
|
||||||
|
}
|
||||||
|
|
||||||
public function testEveryButtonNamesItsProfileForAssistiveTechnology(): void
|
public function testEveryButtonNamesItsProfileForAssistiveTechnology(): void
|
||||||
{
|
{
|
||||||
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
|
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
|
||||||
@@ -430,6 +470,120 @@ final class CompatibilityTabTest extends TestCase
|
|||||||
self::html($this->tab(self::profile(), self::report()));
|
self::html($this->tab(self::profile(), self::report()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTheProfileIntroRunsBeforeTheCapabilityTableAndTheExtraAfterIt(): void
|
||||||
|
{
|
||||||
|
Actions\expectDone(CompatibilityTab::INTRO_ACTION)->once()->with('fake')->whenHappen(static function (): void {
|
||||||
|
echo '[INTRO]';
|
||||||
|
});
|
||||||
|
Actions\expectDone(CompatibilityTab::EXTRA_ACTION)->once()->with('fake')->whenHappen(static function (): void {
|
||||||
|
echo '[EXTRA]';
|
||||||
|
});
|
||||||
|
|
||||||
|
$html = self::html($this->tab(self::profile(), self::report()));
|
||||||
|
|
||||||
|
self::assertLessThan(strpos($html, 'lit-mcp-compat-details'), strpos($html, '[INTRO]'));
|
||||||
|
self::assertGreaterThan(strpos($html, '</table></details>'), strpos($html, '[EXTRA]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnInactiveProfileFiresNeitherIntroNorExtra(): void
|
||||||
|
{
|
||||||
|
Actions\expectDone(CompatibilityTab::INTRO_ACTION)->never();
|
||||||
|
Actions\expectDone(CompatibilityTab::EXTRA_ACTION)->never();
|
||||||
|
|
||||||
|
self::html($this->tab(self::profile(active: false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAProfileCanReplaceTheGenericExplanationAndTheTabEscapesIt(): void
|
||||||
|
{
|
||||||
|
Filters\expectApplied(CompatibilityTab::EXPLANATION_FILTER)
|
||||||
|
->once()
|
||||||
|
->with('Alle Fähigkeiten sind nutzbar.', 'fake', ReleaseState::Released)
|
||||||
|
->andReturn('Eigener <b>Text</b>');
|
||||||
|
|
||||||
|
$html = self::html($this->tab(self::profile()));
|
||||||
|
|
||||||
|
self::assertStringContainsString('Eigener <b>Text</b>', $html);
|
||||||
|
self::assertStringNotContainsString('Alle Fähigkeiten sind nutzbar.', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testANonStringExplanationFromTheFilterKeepsTheGenericText(): void
|
||||||
|
{
|
||||||
|
Filters\expectApplied(CompatibilityTab::EXPLANATION_FILTER)->once()->andReturn(['x']);
|
||||||
|
|
||||||
|
self::assertStringContainsString('Alle Fähigkeiten sind nutzbar.', self::html($this->tab(self::profile())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{heading: string, cell: Closure} */
|
||||||
|
private static function column(string $heading, Closure $cell): array
|
||||||
|
{
|
||||||
|
return ['heading' => $heading, 'cell' => $cell];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidColumnsAreDroppedAndTheRestIsReindexed(): void
|
||||||
|
{
|
||||||
|
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn([
|
||||||
|
3 => self::column('Gut', static fn (): array => ['ok']),
|
||||||
|
5 => ['heading' => 42, 'cell' => static fn (): array => ['x']],
|
||||||
|
6 => ['heading' => 'Ohne Closure', 'cell' => 'strlen'],
|
||||||
|
7 => 'kaputt',
|
||||||
|
9 => ['heading' => 'Ohne Zelle'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = self::html($this->tab(self::profile()));
|
||||||
|
|
||||||
|
self::assertStringContainsString('<th scope="col">Gut</th>', $html);
|
||||||
|
self::assertSame(3, substr_count($html, '<th scope="col">'));
|
||||||
|
self::assertStringContainsString('<td data-label="Gut"><ul><li>ok</li></ul></td>', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAThrowingCellShowsADashLogsTheFailureAndKeepsTheTableClosed(): void
|
||||||
|
{
|
||||||
|
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn([
|
||||||
|
self::column('Kaputt', static function (): array {
|
||||||
|
throw new \RuntimeException('Zelle <kaputt>');
|
||||||
|
}),
|
||||||
|
self::column('Heil', static fn (): array => ['ok']),
|
||||||
|
]);
|
||||||
|
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
||||||
|
$previous = ini_set('error_log', $log);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$html = self::html($this->tab(self::profile()));
|
||||||
|
} finally {
|
||||||
|
ini_set('error_log', (string) $previous);
|
||||||
|
}
|
||||||
|
$logged = (string) file_get_contents($log);
|
||||||
|
unlink($log);
|
||||||
|
|
||||||
|
self::assertStringContainsString('<td data-label="Kaputt">–</td>', $html);
|
||||||
|
self::assertStringContainsString('<td data-label="Heil"><ul><li>ok</li></ul></td>', $html);
|
||||||
|
self::assertStringContainsString('</tbody></table></details>', $html);
|
||||||
|
self::assertStringContainsString('fake capability column "Kaputt" failed', $logged);
|
||||||
|
self::assertStringContainsString('Zelle <kaputt>', $logged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testACellThatReturnsNoListShowsADash(): void
|
||||||
|
{
|
||||||
|
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn([
|
||||||
|
self::column('Text', static fn (): string => 'kein Array'),
|
||||||
|
self::column('Karte', static fn (): array => ['a' => 'x']),
|
||||||
|
self::column('Zahlen', static fn (): array => [1, 2]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = self::html($this->tab(self::profile()));
|
||||||
|
|
||||||
|
self::assertStringContainsString('<td data-label="Text">–</td>', $html);
|
||||||
|
self::assertStringContainsString('<td data-label="Karte">–</td>', $html);
|
||||||
|
self::assertStringContainsString('<td data-label="Zahlen">–</td>', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testANonArrayColumnsFilterResultMeansNoExtraColumns(): void
|
||||||
|
{
|
||||||
|
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn('kaputt');
|
||||||
|
|
||||||
|
self::assertSame(2, substr_count(self::html($this->tab(self::profile())), '<th scope="col">'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testEverythingFromThirdPartiesIsEscaped(): void
|
public function testEverythingFromThirdPartiesIsEscaped(): void
|
||||||
{
|
{
|
||||||
$profile = self::profile(
|
$profile = self::profile(
|
||||||
|
|||||||
@@ -177,31 +177,6 @@ final class SettingsPageTest extends TestCase
|
|||||||
$page->register();
|
$page->register();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider diviNotices */
|
|
||||||
public function testDiviNoticesAreShown(string $key, string $class, string $text): void
|
|
||||||
{
|
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
|
||||||
$_GET['lit_mcp_notice'] = $key;
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->render();
|
|
||||||
$html = (string) ob_get_clean();
|
|
||||||
|
|
||||||
self::assertStringContainsString('notice-' . $class, $html);
|
|
||||||
self::assertStringContainsString($text, $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return list<list<string>> */
|
|
||||||
public static function diviNotices(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
['divi_released', 'success', 'für LIT MCP freigegeben'],
|
|
||||||
['divi_release_refused', 'error', 'Prüfung erneut ausführen'],
|
|
||||||
['divi_withdrawn', 'success', 'Freigabe zurückgezogen'],
|
|
||||||
['divi_withdraw_failed', 'error', 'keine Freigabe'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider compatNotices */
|
/** @dataProvider compatNotices */
|
||||||
public function testCompatNoticesNameTheKnownProfile(string $key, string $class, string $text): void
|
public function testCompatNoticesNameTheKnownProfile(string $key, string $class, string $text): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ final class CoreIndependenceTest extends TestCase
|
|||||||
|
|
||||||
self::assertArrayHasKey('Plugin.php', $files, 'root files are scanned');
|
self::assertArrayHasKey('Plugin.php', $files, 'root files are scanned');
|
||||||
self::assertArrayHasKey('Integrations/IntegrationRegistry.php', $files, 'Integrations root files are scanned');
|
self::assertArrayHasKey('Integrations/IntegrationRegistry.php', $files, 'Integrations root files are scanned');
|
||||||
self::assertArrayNotHasKey('Admin/DiviTab.php', $files, 'the Divi tab belongs to Integrations/Divi/Admin');
|
self::assertArrayNotHasKey('Admin/DiviAdminExtras.php', $files, 'the Divi additions belong to Integrations/Divi/Admin');
|
||||||
self::assertFileExists(self::root() . 'Integrations/Divi/Admin/DiviTab.php');
|
self::assertFileExists(self::root() . 'Integrations/Divi/Admin/DiviAdminExtras.php');
|
||||||
self::assertFileExists(self::root() . 'Integrations/Divi/Cli/DiviCheckCommand.php');
|
self::assertFileExists(self::root() . 'Integrations/Divi/Cli/DiviCheckCommand.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,379 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Admin;
|
||||||
|
|
||||||
|
use Brain\Monkey\Actions;
|
||||||
|
use Brain\Monkey\Filters;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Admin\CompatibilityTab;
|
||||||
|
use LIT\MCP\Compatibility\CheckReport;
|
||||||
|
use LIT\MCP\Compatibility\CheckReportStore;
|
||||||
|
use LIT\MCP\Compatibility\FeatureStatus;
|
||||||
|
use LIT\MCP\Compatibility\ReleaseState;
|
||||||
|
use LIT\MCP\Integrations\Divi\Admin\DiviAdminExtras;
|
||||||
|
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\ReleaseStatus;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
||||||
|
use LIT\MCP\Tests\Support\FeatureFakeTool;
|
||||||
|
use LIT\MCP\Tests\Support\MatrixFixtures;
|
||||||
|
use LIT\MCP\Tests\Support\ReleaseMatrices;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
final class DiviAdminExtrasTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Functions\stubEscapeFunctions();
|
||||||
|
Functions\stubTranslationFunctions();
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://k.de/wp-admin/' . $path);
|
||||||
|
Functions\when('add_query_arg')->alias(
|
||||||
|
static fn (array $args, string $url): string => $url . '?' . http_build_query($args)
|
||||||
|
);
|
||||||
|
Functions\when('sanitize_key')->alias(static fn (string $key): string => strtolower($key));
|
||||||
|
Functions\when('wp_unslash')->returnArg();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
unset($_GET['tab']);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function extras(
|
||||||
|
string $version = '5.3.0',
|
||||||
|
?FeatureOverview $overview = null,
|
||||||
|
bool $report = false
|
||||||
|
): DiviAdminExtras {
|
||||||
|
$reports = Mockery::mock(CheckReportStore::class);
|
||||||
|
$reports->allows('latest')->andReturn($report ? new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, '5.3.0', '0.7.0', 'key', []) : null);
|
||||||
|
|
||||||
|
return new DiviAdminExtras($overview, static fn (): string => $version, $reports);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ReleaseStatus::of('5.3.0', $matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function extraHtml(ReleaseStatus $status, ?DiviAdminExtras $extras = null, string $id = 'divi'): string
|
||||||
|
{
|
||||||
|
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
($extras ?? self::extras())->renderExtras($id);
|
||||||
|
|
||||||
|
return (string) ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function introHtml(ReleaseStatus $status, ?DiviAdminExtras $extras = null, string $id = 'divi'): string
|
||||||
|
{
|
||||||
|
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
($extras ?? self::extras())->renderIntro($id);
|
||||||
|
|
||||||
|
return (string) ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function explanationOf(ReleaseStatus $status, string $id = 'divi', string $generic = 'generisch'): string
|
||||||
|
{
|
||||||
|
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||||
|
|
||||||
|
return self::extras()->explanation($generic, $id, $status->state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function noticeHtml(ReleaseStatus $status, string $version = '5.3.0'): string
|
||||||
|
{
|
||||||
|
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
self::extras($version)->renderNotice();
|
||||||
|
|
||||||
|
return (string) ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItHooksIntoTheCompatibilityTabAndTheAdminNotices(): void
|
||||||
|
{
|
||||||
|
$extras = self::extras();
|
||||||
|
Actions\expectAdded('admin_notices')->once()->with([$extras, 'renderNotice']);
|
||||||
|
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->once()->with([$extras, 'removeOrphans']);
|
||||||
|
Actions\expectAdded(CompatibilityTab::INTRO_ACTION)->once()->with([$extras, 'renderIntro']);
|
||||||
|
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once()->with([$extras, 'renderExtras']);
|
||||||
|
Filters\expectAdded(CompatibilityTab::EXPLANATION_FILTER)->once()->with([$extras, 'explanation'], 10, 3);
|
||||||
|
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once()->with([$extras, 'columns'], 10, 2);
|
||||||
|
|
||||||
|
$extras->register();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheNoticeNamesTheUnusableFeaturesAndLinksToTheCompatibilityTab(): void
|
||||||
|
{
|
||||||
|
$html = self::noticeHtml(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=compatibility', $html);
|
||||||
|
self::assertStringContainsString('Reiter „Compatibility“', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoNoticeWhileEveryFeatureIsUsable(): void
|
||||||
|
{
|
||||||
|
self::assertSame('', self::noticeHtml(ReleaseMatrices::released('5.3.0')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoNoticeForUsersWithoutManageOptions(): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->justReturn(false);
|
||||||
|
|
||||||
|
self::assertSame('', self::noticeHtml(self::mixedStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoNoticeWithoutDivi(): void
|
||||||
|
{
|
||||||
|
self::assertSame('', self::noticeHtml(self::mixedStatus(), ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoNoticeOnTheCompatibilityTabItself(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_current_screen')->justReturn((object) ['id' => 'settings_page_lit-mcp']);
|
||||||
|
$_GET['tab'] = 'compatibility';
|
||||||
|
|
||||||
|
self::assertSame('', self::noticeHtml(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', self::noticeHtml(self::mixedStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The orphans go in the load action of the tab, never while rendering. */
|
||||||
|
public function testTheLoadActionRemovesOrphansAndTheExtrasReportThem(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_posts')->once()->andReturn([
|
||||||
|
new WP_Post(['ID' => 51, 'post_title' => WriteProbe::TITLE . ' 2026-10-18 08:00']),
|
||||||
|
]);
|
||||||
|
Functions\expect('wp_delete_post')->once()->with(51, true)->andReturn(new WP_Post(['ID' => 51]));
|
||||||
|
$extras = self::extras();
|
||||||
|
|
||||||
|
$extras->removeOrphans('divi');
|
||||||
|
$html = self::introHtml(ReleaseMatrices::released('5.3.0'), $extras);
|
||||||
|
|
||||||
|
self::assertStringContainsString('Liegengebliebene Prüfseiten gelöscht: 51', $html);
|
||||||
|
self::assertStringNotContainsString('Liegengebliebene', self::extraHtml(ReleaseMatrices::released('5.3.0'), $extras));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRenderingAloneDeletesNothing(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_posts')->never();
|
||||||
|
Functions\expect('wp_delete_post')->never();
|
||||||
|
|
||||||
|
$html = self::introHtml(ReleaseMatrices::released('5.3.0'));
|
||||||
|
|
||||||
|
self::assertStringNotContainsString('Liegengebliebene Prüfseiten', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheLoadActionDeletesNothingForOtherProfilesWithoutTheRightOrWithoutDivi(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_posts')->never();
|
||||||
|
|
||||||
|
self::extras()->removeOrphans('pods');
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->justReturn(false);
|
||||||
|
self::extras()->removeOrphans('divi');
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
self::extras('')->removeOrphans('divi');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheIntroAndExtrasOfOtherProfilesAreEmpty(): void
|
||||||
|
{
|
||||||
|
self::assertSame('', self::extraHtml(ReleaseMatrices::released('5.3.0'), null, 'pods'));
|
||||||
|
self::assertSame('', self::introHtml(self::mixedStatus(), null, 'pods'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheExtrasExplainTheAutomationTheFilterAndTheVisualBuilderWithAReport(): void
|
||||||
|
{
|
||||||
|
$html = self::extraHtml(ReleaseMatrices::released('5.3.0'), self::extras('5.3.0', null, true));
|
||||||
|
|
||||||
|
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);
|
||||||
|
self::assertStringContainsString('Visual Builder', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheVisualBuilderHintNeedsAReport(): void
|
||||||
|
{
|
||||||
|
$html = self::extraHtml(ReleaseMatrices::released('5.3.0'));
|
||||||
|
|
||||||
|
self::assertStringNotContainsString('Visual Builder', $html);
|
||||||
|
self::assertStringContainsString('gibt LIT MCP jede Fähigkeit einzeln frei', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheIntroSaysWhatTheCheckDoes(): void
|
||||||
|
{
|
||||||
|
$html = self::introHtml(ReleaseMatrices::released('5.3.0'));
|
||||||
|
|
||||||
|
self::assertStringContainsString('legt eine private Testseite an, prüft Lesen und Schreiben und löscht sie wieder', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAReleasedVersionNeedsNoNextStep(): void
|
||||||
|
{
|
||||||
|
self::assertStringNotContainsString('notice notice-', self::introHtml(ReleaseMatrices::released('5.3.0')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnIncompatibleVersionNamesThePluginUpdateAsTheNextStep(): void
|
||||||
|
{
|
||||||
|
$html = self::introHtml(self::mixedStatus());
|
||||||
|
|
||||||
|
self::assertStringContainsString('notice notice-error inline', $html);
|
||||||
|
self::assertStringContainsString('Plugin-Update', $html);
|
||||||
|
self::assertStringNotContainsString('lesen nur', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnUntestedVersionNamesTheCheckAsTheNextStep(): void
|
||||||
|
{
|
||||||
|
$html = self::introHtml(ReleaseMatrices::uniform(ReleaseState::Untested));
|
||||||
|
|
||||||
|
self::assertStringContainsString('notice notice-warning inline', $html);
|
||||||
|
self::assertStringContainsString('Divi 5.3.0 ist noch nicht geprüft. Starte die Prüfung', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheExplanationNamesTheSourceOfAReleasedVersion(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Jan <Admin>']);
|
||||||
|
Functions\when('get_option')->justReturn('Y-m-d');
|
||||||
|
Functions\when('date_i18n')->alias(static fn (string $format, int $time): string => gmdate($format, $time));
|
||||||
|
|
||||||
|
$code = self::explanationOf(ReleaseStatus::of('5.3.0', MatrixFixtures::released('code')));
|
||||||
|
$auto = self::explanationOf(ReleaseStatus::of('5.3.0', MatrixFixtures::released('auto')));
|
||||||
|
$database = self::explanationOf(ReleaseStatus::of(
|
||||||
|
'5.3.0',
|
||||||
|
MatrixFixtures::released('database'),
|
||||||
|
['user_id' => 7, 'released_at' => '2026-10-01T08:00:00+00:00']
|
||||||
|
));
|
||||||
|
|
||||||
|
self::assertSame('Diese Version ist im Plugin als getestet hinterlegt.', $code);
|
||||||
|
self::assertStringStartsWith('Automatisch freigegeben: Alle Divi-Stellen', $auto);
|
||||||
|
self::assertSame('Auf dieser Website freigegeben am 2026-10-01 von Jan <Admin>.', $database);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheExplanationOfADatabaseReleaseWithoutAKnownUserNamesAQuestionMark(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_userdata')->justReturn(false);
|
||||||
|
Functions\when('get_option')->justReturn('Y-m-d');
|
||||||
|
Functions\when('date_i18n')->alias(static fn (string $format, int $time): string => gmdate($format, $time));
|
||||||
|
|
||||||
|
$text = self::explanationOf(ReleaseStatus::of('5.3.0', MatrixFixtures::released('database'), ['user_id' => 7]));
|
||||||
|
|
||||||
|
self::assertStringEndsWith('von ?.', $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheExplanationHasOwnTextsForUntestedAndIncompatible(): void
|
||||||
|
{
|
||||||
|
$untested = self::explanationOf(ReleaseMatrices::uniform(ReleaseState::Untested));
|
||||||
|
$incompatible = self::explanationOf(self::mixedStatus());
|
||||||
|
|
||||||
|
self::assertStringContainsString('Diese Version ist nicht geprüft. Die Tools der ungetesteten Fähigkeiten', $untested);
|
||||||
|
self::assertStringContainsString('bis ein Plugin-Update von LIT MCP eingespielt ist', $incompatible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheExplanationOfOtherProfilesStaysGeneric(): void
|
||||||
|
{
|
||||||
|
self::assertSame('generisch', self::explanationOf(self::mixedStatus(), 'pods'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{heading: string, cell: callable, code?: bool}> */
|
||||||
|
private static function columns(DiviAdminExtras $extras, string $id = 'divi'): array
|
||||||
|
{
|
||||||
|
return $extras->columns([], $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<list<string>> The cells of one capability across the three columns. */
|
||||||
|
private static function cellsOf(DiviFeature $feature, array $columns, ReleaseStatus $status): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (array $column): array => ($column['cell'])($feature, $status->matrix), $columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheCapabilityTableGetsSourceProblemsAndAffectedTools(): 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]),
|
||||||
|
]);
|
||||||
|
$columns = self::columns(self::extras('5.3.0', $overview));
|
||||||
|
$status = self::mixedStatus();
|
||||||
|
|
||||||
|
self::assertSame(['Quelle', 'Probleme', 'Betroffene Tools'], array_column($columns, 'heading'));
|
||||||
|
self::assertTrue($columns[2]['code']);
|
||||||
|
self::assertSame(
|
||||||
|
[[], ['Seiten: Signatur von et_pb_x() <geändert>'], ['lit-divi-insert']],
|
||||||
|
self::cellsOf(DiviFeature::PageWrite, $columns, $status)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[['Automatisch geprüft'], [], ['lit-divi-insert']],
|
||||||
|
self::cellsOf(DiviFeature::PageRead, $columns, $status)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
['lit/divi-presets-assign'],
|
||||||
|
self::cellsOf(DiviFeature::PresetWrite, $columns, $status)[2]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheColumnsNameEverySourceInWordsAndKeepTheOthersAndOtherProfiles(): void
|
||||||
|
{
|
||||||
|
// One column set per table: it keeps the rows of the first matrix it sees.
|
||||||
|
$code = self::cellsOf(DiviFeature::PageRead, self::columns(self::extras()), ReleaseMatrices::released('5.3.0'));
|
||||||
|
$database = self::cellsOf(
|
||||||
|
DiviFeature::PageRead,
|
||||||
|
self::columns(self::extras()),
|
||||||
|
ReleaseStatus::of('5.3.0', MatrixFixtures::released('database'))
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(['Im Plugin als getestet hinterlegt'], $code[0]);
|
||||||
|
self::assertSame(['Von dir auf dieser Website freigegeben'], $database[0]);
|
||||||
|
self::assertSame([['heading' => 'x']], self::extras()->columns([['heading' => 'x']], 'pods'));
|
||||||
|
self::assertCount(4, self::extras()->columns([['heading' => 'x']], 'divi'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheColumnsReadTheOverviewOncePerTable(): void
|
||||||
|
{
|
||||||
|
$calls = 0;
|
||||||
|
$overview = new FeatureOverview(static function () use (&$calls): array {
|
||||||
|
++$calls;
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
$columns = self::columns(self::extras('5.3.0', $overview));
|
||||||
|
$status = ReleaseMatrices::released('5.3.0');
|
||||||
|
|
||||||
|
foreach (DiviFeature::cases() as $feature) {
|
||||||
|
self::cellsOf($feature, $columns, $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame(1, $calls);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,561 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Admin;
|
|
||||||
|
|
||||||
use Brain\Monkey\Actions;
|
|
||||||
use Brain\Monkey\Functions;
|
|
||||||
use LIT\MCP\Admin\AdminPostAction;
|
|
||||||
use LIT\MCP\Compatibility\ReleaseState;
|
|
||||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
|
||||||
use LIT\MCP\Compatibility\CheckReport;
|
|
||||||
use LIT\MCP\Compatibility\CheckReportStore;
|
|
||||||
use LIT\MCP\Compatibility\CheckStep;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
|
||||||
use LIT\MCP\Compatibility\FeatureStatus;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
|
||||||
use LIT\MCP\Compatibility\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\Support\ReleaseMatrices;
|
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
|
||||||
use Mockery;
|
|
||||||
use WP_Post;
|
|
||||||
|
|
||||||
final class DiviTabTest extends TestCase
|
|
||||||
{
|
|
||||||
private ?string $redirect = null;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->redirect = null;
|
|
||||||
Functions\stubEscapeFunctions();
|
|
||||||
Functions\stubTranslationFunctions();
|
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
|
||||||
Functions\when('check_admin_referer')->justReturn(1);
|
|
||||||
Functions\when('get_current_user_id')->justReturn(1);
|
|
||||||
Functions\when('wp_safe_redirect')->alias(function (string $url): bool {
|
|
||||||
$this->redirect = $url;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://k.de/wp-admin/' . $path);
|
|
||||||
Functions\when('add_query_arg')->alias(
|
|
||||||
static fn (array $args, string $url): string => $url . '?' . http_build_query($args)
|
|
||||||
);
|
|
||||||
Functions\when('sanitize_key')->alias(static fn (string $key): string => strtolower($key));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
unset($_GET['tab']);
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function report(bool $green = true, string $version = '5.3.0', string $key = 'key'): CheckReport
|
|
||||||
{
|
|
||||||
$steps = array_map(
|
|
||||||
static fn (string $step): CheckStep => new CheckStep($step, $green ? [] : ['rot']),
|
|
||||||
[CheckStep::MANIFEST, CheckStep::MIRRORS, DiviCheckSteps::CATALOG, DiviCheckSteps::FORMAT, DiviCheckSteps::WRITE]
|
|
||||||
);
|
|
||||||
|
|
||||||
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,
|
|
||||||
?FeatureOverview $overview = null
|
|
||||||
): DiviTab {
|
|
||||||
$reports = Mockery::mock(CheckReportStore::class);
|
|
||||||
$reports->allows('latest')->andReturn($report);
|
|
||||||
|
|
||||||
return new DiviTab(
|
|
||||||
$reports,
|
|
||||||
$releases,
|
|
||||||
$log ?? FakeChangeLog::forbidden(),
|
|
||||||
new AdminPostAction(static function (): void {
|
|
||||||
}),
|
|
||||||
static fn (): string => '5.3.0',
|
|
||||||
static fn (string $version): string => 'key',
|
|
||||||
$overview
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReleaseNeedsAFreshGreenReport(): void
|
|
||||||
{
|
|
||||||
$reports = [null, self::report(false), self::report(true, '5.2.9'), self::report(true, '5.3.0', 'old')];
|
|
||||||
foreach ($reports as $report) {
|
|
||||||
$this->redirect = null;
|
|
||||||
$releases = Mockery::mock(ReleaseStore::class);
|
|
||||||
$releases->expects('add')->never();
|
|
||||||
|
|
||||||
$this->tab($report, $releases)->release();
|
|
||||||
|
|
||||||
self::assertStringContainsString('lit_mcp_notice=divi_release_refused', (string) $this->redirect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReleaseStoresTheEntryAndLogsIt(): void
|
|
||||||
{
|
|
||||||
$releases = Mockery::mock(ReleaseStore::class);
|
|
||||||
$releases->expects('add')->once()->with('5.3.0', Plugin::VERSION, 1, 'r1');
|
|
||||||
$log = new FakeChangeLog();
|
|
||||||
|
|
||||||
$this->tab(self::report(), $releases, $log)->release();
|
|
||||||
|
|
||||||
self::assertStringContainsString('lit_mcp_notice=divi_released', (string) $this->redirect);
|
|
||||||
self::assertSame('lit/divi-release', $log->records[0]['ability']);
|
|
||||||
self::assertStringContainsString('Divi 5.3.0', $log->records[0]['summary']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithdrawRemovesTheEntryOfTheInstalledVersion(): void
|
|
||||||
{
|
|
||||||
$releases = Mockery::mock(ReleaseStore::class);
|
|
||||||
$releases->expects('withdraw')->once()->with('5.3.0', Plugin::VERSION)->andReturn(true);
|
|
||||||
$log = new FakeChangeLog();
|
|
||||||
|
|
||||||
$this->tab(null, $releases, $log)->withdraw();
|
|
||||||
|
|
||||||
self::assertStringContainsString('lit_mcp_notice=divi_withdrawn', (string) $this->redirect);
|
|
||||||
self::assertSame('lit/divi-release', $log->records[0]['ability']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testActionsNeedNonceAndManageOptions(): void
|
|
||||||
{
|
|
||||||
Functions\when('current_user_can')->justReturn(false);
|
|
||||||
Functions\expect('wp_die')->twice()->andReturnUsing(static function (): never {
|
|
||||||
throw new \RuntimeException('died');
|
|
||||||
});
|
|
||||||
$releases = Mockery::mock(ReleaseStore::class);
|
|
||||||
$releases->expects('add')->never();
|
|
||||||
$releases->expects('withdraw')->never();
|
|
||||||
$tab = $this->tab(self::report(), $releases);
|
|
||||||
|
|
||||||
foreach (['release', 'withdraw'] as $method) {
|
|
||||||
try {
|
|
||||||
$tab->{$method}();
|
|
||||||
self::fail($method . ' without manage_options');
|
|
||||||
} catch (\RuntimeException $e) {
|
|
||||||
self::assertSame('died', $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReleaseChecksTheNonce(): void
|
|
||||||
{
|
|
||||||
Functions\when('check_admin_referer')->alias(static function (string $action): never {
|
|
||||||
throw new \RuntimeException('bad nonce for ' . $action);
|
|
||||||
});
|
|
||||||
$releases = Mockery::mock(ReleaseStore::class);
|
|
||||||
$releases->expects('add')->never();
|
|
||||||
|
|
||||||
$this->expectExceptionMessage('bad nonce for lit_mcp_divi_release');
|
|
||||||
$this->tab(self::report(), $releases)->release();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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');
|
|
||||||
Functions\when('wp_create_nonce')->justReturn('n');
|
|
||||||
Functions\when('wp_nonce_field')->justReturn('');
|
|
||||||
Functions\when('submit_button')->alias(static function (string $text, string $type = 'primary'): void {
|
|
||||||
echo '<button class="' . $type . '">' . $text . '</button>';
|
|
||||||
});
|
|
||||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Jan']);
|
|
||||||
Functions\when('date_i18n')->justReturn('01.10.2026 10:00');
|
|
||||||
Functions\when('get_option')->justReturn('d.m.Y');
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
$tab->render();
|
|
||||||
|
|
||||||
return (string) ob_get_clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testUntestedWithAFreshGreenReportOffersTheRelease(): void
|
|
||||||
{
|
|
||||||
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report());
|
|
||||||
|
|
||||||
self::assertStringContainsString('ungetestet', $html);
|
|
||||||
self::assertStringContainsString('Prüfung starten', $html);
|
|
||||||
self::assertStringContainsString('Divi 5.3.0 freigeben', $html);
|
|
||||||
self::assertStringContainsString('name="_wpnonce" value="n"', $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCheckButtonSaysWhatItDoesAndLeadsWhileNothingElseCanBeDone(): void
|
|
||||||
{
|
|
||||||
$stale = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report(true, '5.2.9'));
|
|
||||||
$fresh = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report());
|
|
||||||
$released = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report());
|
|
||||||
|
|
||||||
foreach ([$stale, $fresh, $released] as $html) {
|
|
||||||
self::assertStringContainsString('legt eine private Testseite an', $html);
|
|
||||||
}
|
|
||||||
self::assertStringContainsString('<button class="primary">Prüfung starten</button>', $stale);
|
|
||||||
self::assertStringContainsString('<button class="secondary">Prüfung starten</button>', $fresh);
|
|
||||||
self::assertStringContainsString('<button class="primary">Divi 5.3.0 freigeben</button>', $fresh);
|
|
||||||
self::assertStringContainsString('<button class="secondary">Prüfung starten</button>', $released);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testUntestedWithAStaleReportAsksForANewCheck(): void
|
|
||||||
{
|
|
||||||
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report(true, '5.2.9'));
|
|
||||||
|
|
||||||
self::assertStringNotContainsString('Divi 5.3.0 freigeben', $html);
|
|
||||||
self::assertStringContainsString('Prüfung erneut ausführen', $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testReleasedFromCodeOffersNeitherReleaseNorWithdrawal(): void
|
|
||||||
{
|
|
||||||
$html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report());
|
|
||||||
|
|
||||||
self::assertStringContainsString('freigegeben', $html);
|
|
||||||
self::assertStringContainsString('im Plugin als getestet hinterlegt', $html);
|
|
||||||
self::assertStringNotContainsString('freigeben</button>', $html);
|
|
||||||
self::assertStringNotContainsString('Freigabe zurückziehen', $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAnAutomaticReleaseSaysSoAndOffersNeitherReleaseNorWithdrawal(): void
|
|
||||||
{
|
|
||||||
$html = $this->rendered(ReleaseMatrices::released('5.99.0', ReleaseStatus::SOURCE_AUTO), self::report());
|
|
||||||
|
|
||||||
self::assertStringContainsString('Automatisch freigegeben', $html);
|
|
||||||
self::assertStringNotContainsString('freigeben</button>', $html);
|
|
||||||
self::assertStringNotContainsString('Freigabe zurückziehen', $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testADatabaseReleaseCanBeWithdrawn(): void
|
|
||||||
{
|
|
||||||
$status = ReleaseMatrices::released('5.3.0', ReleaseStatus::SOURCE_DATABASE, [
|
|
||||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::assertStringContainsString('Freigabe zurückziehen', $this->rendered($status, self::report()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testWithoutDiviShowsOnlyAShortNotice(): void
|
|
||||||
{
|
|
||||||
ReleaseGate::useResolver(static function (): ReleaseStatus {
|
|
||||||
self::fail('resolver called although Divi is not active');
|
|
||||||
});
|
|
||||||
$reports = Mockery::mock(CheckReportStore::class);
|
|
||||||
$reports->expects('latest')->never();
|
|
||||||
$tab = new DiviTab(
|
|
||||||
$reports,
|
|
||||||
Mockery::mock(ReleaseStore::class),
|
|
||||||
FakeChangeLog::forbidden(),
|
|
||||||
new AdminPostAction(static function (): void {
|
|
||||||
}),
|
|
||||||
static fn (): string => '',
|
|
||||||
static fn (string $version): string => 'key'
|
|
||||||
);
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
$tab->render();
|
|
||||||
$html = (string) ob_get_clean();
|
|
||||||
|
|
||||||
self::assertStringContainsString(
|
|
||||||
'Divi ist auf dieser Website nicht aktiv; die Divi-Freigabe ist ohne Bedeutung.',
|
|
||||||
$html
|
|
||||||
);
|
|
||||||
self::assertStringNotContainsString('Prüfung starten', $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testIncompatibleListsTheProblems(): void
|
|
||||||
{
|
|
||||||
$html = $this->rendered(
|
|
||||||
ReleaseMatrices::uniform(ReleaseState::Incompatible, '5.3.0', ['Theme Builder: Nachbau von x() – y']),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertStringContainsString('inkompatibel', $html);
|
|
||||||
self::assertStringContainsString('Theme Builder: Nachbau von x() – y', $html);
|
|
||||||
self::assertStringContainsString('Plugin-Update', $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The tab removes the pages of a dead check in the settings page's load hook, never while rendering. */
|
|
||||||
public function testRegistersTheCleanupOnTheSettingsPageLoadHook(): void
|
|
||||||
{
|
|
||||||
Actions\expectAdded('admin_post_lit_mcp_divi_release')->once();
|
|
||||||
Actions\expectAdded('admin_post_lit_mcp_divi_withdraw')->once();
|
|
||||||
Actions\expectAdded('load-settings_page_lit-mcp')->once()->with(Mockery::type('array'));
|
|
||||||
|
|
||||||
$this->tab(null, Mockery::mock(ReleaseStore::class))->registerActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheLoadHookOnThisTabRemovesOrphansAndTheTabReportsThem(): void
|
|
||||||
{
|
|
||||||
$_GET['tab'] = 'divi';
|
|
||||||
Functions\expect('get_posts')->once()->andReturn([
|
|
||||||
new WP_Post(['ID' => 51, 'post_title' => WriteProbe::TITLE . ' 2026-10-18 08:00']),
|
|
||||||
]);
|
|
||||||
Functions\expect('wp_delete_post')->once()->with(51, true)->andReturn(new WP_Post(['ID' => 51]));
|
|
||||||
$tab = $this->tab(self::report(), Mockery::mock(ReleaseStore::class));
|
|
||||||
|
|
||||||
$tab->removeOrphans();
|
|
||||||
$html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report(), $tab);
|
|
||||||
|
|
||||||
self::assertStringContainsString('Liegengebliebene Prüfseiten gelöscht: 51', $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRenderingAloneDeletesNothing(): void
|
|
||||||
{
|
|
||||||
Functions\expect('get_posts')->never();
|
|
||||||
Functions\expect('wp_delete_post')->never();
|
|
||||||
|
|
||||||
$html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report());
|
|
||||||
|
|
||||||
self::assertStringNotContainsString('Liegengebliebene Prüfseiten', $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheLoadHookDeletesNothingOnOtherTabsWithoutTheRightOrWithoutDivi(): void
|
|
||||||
{
|
|
||||||
Functions\expect('get_posts')->never();
|
|
||||||
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
|
|
||||||
|
|
||||||
unset($_GET['tab']);
|
|
||||||
$tab->removeOrphans();
|
|
||||||
$_GET['tab'] = 'tools';
|
|
||||||
$tab->removeOrphans();
|
|
||||||
|
|
||||||
$_GET['tab'] = 'divi';
|
|
||||||
Functions\when('current_user_can')->justReturn(false);
|
|
||||||
$tab->removeOrphans();
|
|
||||||
|
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
|
||||||
(new DiviTab(
|
|
||||||
Mockery::mock(CheckReportStore::class),
|
|
||||||
Mockery::mock(ReleaseStore::class),
|
|
||||||
FakeChangeLog::forbidden(),
|
|
||||||
null,
|
|
||||||
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 ReleaseStatus::of('5.3.0', $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 Tools</th>', $html);
|
|
||||||
self::assertStringContainsString(
|
|
||||||
'<td data-label="Fähigkeit">Seiten schreiben</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-dismiss"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span> <strong>inkompatibel</strong></span></td><td data-label="Quelle">–</td>',
|
|
||||||
$html
|
|
||||||
);
|
|
||||||
self::assertStringContainsString('<td data-label="Fähigkeit">Modulkatalog</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-warning"><span class="dashicons dashicons-warning" aria-hidden="true"></span> <strong>ungetestet</strong></span></td>', $html);
|
|
||||||
self::assertStringContainsString(
|
|
||||||
'<td data-label="Fähigkeit">Seiten lesen</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-yes-alt"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span> <strong>freigegeben</strong></span></td><td data-label="Quelle">Automatisch geprüft</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(ReleaseMatrices::released('5.3.0'), null);
|
|
||||||
$database = $this->rendered(
|
|
||||||
ReleaseStatus::of('5.3.0', MatrixFixtures::released('database'), [
|
|
||||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
|
||||||
]),
|
|
||||||
self::report()
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertStringContainsString('Im Plugin als getestet hinterlegt', $code);
|
|
||||||
self::assertStringContainsString('Von dir auf dieser Website freigegeben', $database);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheTabExplainsTheAutomationAndTheFilter(): void
|
|
||||||
{
|
|
||||||
$html = $this->rendered(ReleaseMatrices::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(
|
|
||||||
ReleaseStatus::of('5.3.0', 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 = ReleaseStatus::of('5.3.0', $matrix, [
|
|
||||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
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(ReleaseMatrices::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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNamesTheNextStepAboveTheDetailsAndKeepsAReleasedTableFolded(): void
|
|
||||||
{
|
|
||||||
$incompatible = $this->rendered(self::mixedStatus(), null);
|
|
||||||
$released = $this->rendered(ReleaseMatrices::released('5.3.0'), null);
|
|
||||||
|
|
||||||
self::assertStringContainsString('notice notice-error inline', $incompatible);
|
|
||||||
self::assertStringContainsString('<details class="lit-mcp-divi-details" open>', $incompatible);
|
|
||||||
self::assertStringNotContainsString('notice notice-', $released);
|
|
||||||
self::assertStringContainsString('<details class="lit-mcp-divi-details">', $released);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testUntestedVersionNamesTheCheckAsTheNextStep(): void
|
|
||||||
{
|
|
||||||
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), null);
|
|
||||||
|
|
||||||
self::assertStringContainsString('notice notice-warning inline', $html);
|
|
||||||
self::assertStringContainsString('Divi 5.3.0 ist noch nicht geprüft. Starte unten die Prüfung', $html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,7 @@ use Brain\Monkey\Functions;
|
|||||||
use LIT\MCP\Abilities\Ability;
|
use LIT\MCP\Abilities\Ability;
|
||||||
use LIT\MCP\Abilities\ExposurePolicy;
|
use LIT\MCP\Abilities\ExposurePolicy;
|
||||||
use LIT\MCP\Abilities\ToolAvailability;
|
use LIT\MCP\Abilities\ToolAvailability;
|
||||||
use LIT\MCP\Admin\AdminTab;
|
use LIT\MCP\Admin\CompatibilityTab;
|
||||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
|
||||||
use LIT\MCP\Admin\SettingsPage;
|
use LIT\MCP\Admin\SettingsPage;
|
||||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||||
@@ -314,11 +313,10 @@ final class DiviIntegrationTest extends TestCase
|
|||||||
(new DiviIntegration($schema, FakeChangeLog::forbidden(), new ToolAvailability()))->libraryIdsVisible();
|
(new DiviIntegration($schema, FakeChangeLog::forbidden(), new ToolAvailability()))->libraryIdsVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bootable: Divi's own hooks – upgrader cache reset, check route, tab "Divi" after the plugin's tabs. */
|
/** Bootable: Divi's own hooks – upgrader cache reset, check route, extras in the tab "Compatibility" (no own tab). */
|
||||||
public function testBootRegistersDivisOwnHooks(): void
|
public function testBootRegistersDivisOwnHooks(): void
|
||||||
{
|
{
|
||||||
$upgraded = null;
|
$upgraded = null;
|
||||||
$tabs = null;
|
|
||||||
Actions\expectAdded('upgrader_process_complete')->once()->whenHappen(
|
Actions\expectAdded('upgrader_process_complete')->once()->whenHappen(
|
||||||
static function (callable $callback) use (&$upgraded): void {
|
static function (callable $callback) use (&$upgraded): void {
|
||||||
$upgraded = $callback;
|
$upgraded = $callback;
|
||||||
@@ -330,13 +328,13 @@ final class DiviIntegrationTest extends TestCase
|
|||||||
$changed = $callback;
|
$changed = $callback;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->once()->whenHappen(
|
// The Divi admin extras hook into the admin notices and the tab "Compatibility"; Divi has no tab of its own.
|
||||||
static function (callable $callback) use (&$tabs): void {
|
Filters\expectAdded(SettingsPage::TABS_FILTER)->never();
|
||||||
$tabs = $callback;
|
Actions\expectAdded('admin_notices')->once();
|
||||||
}
|
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->once();
|
||||||
);
|
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once();
|
||||||
|
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once();
|
||||||
Functions\expect('delete_transient')->once()->with(ReleaseResolver::CACHE)->andReturn(true);
|
Functions\expect('delete_transient')->once()->with(ReleaseResolver::CACHE)->andReturn(true);
|
||||||
$base = Mockery::mock(AdminTab::class);
|
|
||||||
|
|
||||||
(new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->boot();
|
(new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->boot();
|
||||||
self::assertIsCallable($upgraded);
|
self::assertIsCallable($upgraded);
|
||||||
@@ -345,12 +343,6 @@ final class DiviIntegrationTest extends TestCase
|
|||||||
self::assertIsCallable($changed);
|
self::assertIsCallable($changed);
|
||||||
$changed('pods');
|
$changed('pods');
|
||||||
$changed('divi');
|
$changed('divi');
|
||||||
self::assertIsCallable($tabs);
|
|
||||||
$all = $tabs([$base]);
|
|
||||||
|
|
||||||
self::assertCount(2, $all);
|
|
||||||
self::assertSame($base, $all[0]);
|
|
||||||
self::assertInstanceOf(DiviTab::class, $all[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTheFeatureOverviewFindsTheToolsOfAllIntegrations(): void
|
public function testTheFeatureOverviewFindsTheToolsOfAllIntegrations(): void
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ final class ReleaseStatusTest extends TestCase
|
|||||||
|
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
'Achtung: Divi 5.3.0 ist auf dieser Website für „Seiten schreiben“ noch nicht für LIT MCP freigegeben. '
|
'Achtung: Divi 5.3.0 ist auf dieser Website für „Seiten schreiben“ noch nicht für LIT MCP freigegeben. '
|
||||||
. 'Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung ausführt '
|
. 'Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility die Prüfung ausführt '
|
||||||
. 'und die Version freigibt. Es wurde nichts geändert.',
|
. 'und die Version freigibt. Es wurde nichts geändert.',
|
||||||
$payload['message']
|
$payload['message']
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,12 +53,15 @@ final class PluginTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Only the Divi integration has boot steps (upgrader hook, compat listener, tab); a second boot registers nothing twice. */
|
/** Only Divi has boot steps (upgrader hook, compat listener, admin extras); a second boot registers nothing twice. */
|
||||||
public function testTheBootableIntegrationsBootExactlyOnce(): void
|
public function testTheBootableIntegrationsBootExactlyOnce(): void
|
||||||
{
|
{
|
||||||
Actions\expectAdded('upgrader_process_complete')->once();
|
Actions\expectAdded('upgrader_process_complete')->once();
|
||||||
Actions\expectAdded('lit_mcp_compat_changed')->once();
|
Actions\expectAdded('lit_mcp_compat_changed')->once();
|
||||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->once();
|
Filters\expectAdded(SettingsPage::TABS_FILTER)->never();
|
||||||
|
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->once();
|
||||||
|
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once();
|
||||||
|
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once();
|
||||||
$registry = self::registry();
|
$registry = self::registry();
|
||||||
|
|
||||||
$registry->boot();
|
$registry->boot();
|
||||||
|
|||||||
Reference in New Issue
Block a user