feat(admin): Reiter „Kompatibilität“ übersetzt und übersichtlicher

Sichtbarer Name „Kompatibilität“ (Slug compatibility bleibt) in Reiter, Hinweisen, Hilfe-Tab, CLI-Hilfe, Guides und readme. Oben eine Übersicht aller Plugins mit Gesamtzustand, Version, Zustand (Wort und Icon), Freigabe-Art in Klartext und Sprunglink; je Plugin erst Zustand und nächster Schritt, dann Aktionen, dann aufklappbare Details (offen bei Handlungsbedarf, ohne JavaScript). Tabellen mit Beschriftung und Kartenansicht bis 375 px, Buttons mit Plugin-Namen im zugänglichen Namen, Kontraste mit Reserve. Doku: Divi leert beim Prüfen den Cache einmal komplett.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 17:39:47 +01:00
co-authored by Claude Opus 5.5
parent 661d3945fd
commit f337a57f26
15 changed files with 507 additions and 100 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# End-to-end checks of the Pods profile of the compatibility layer (plan 2026-11-06-kompatibilitaetsschicht, 1.25):
# status, check route /pods-check, tab "Compatibility", release (refused for a tested version, simulated for an
# status, check route /pods-check, tab "Kompatibilität", release (refused for a tested version, simulated for an
# untested one) and the lock by a red stored report. Runs INSIDE the DDEV web container:
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/pods-compat-smoke.sh
# Temporarily activates the installed Pods plugin (restored afterwards); removes the lit_mcp_pods_* options, the
+185 -15
View File
@@ -157,6 +157,14 @@ final class CompatibilityTabTest extends TestCase
);
}
/** The part of $html from the first $from to the next $to (the markers included). */
private static function between(string $html, string $from, string $to): string
{
$start = (int) strpos($html, $from);
return substr($html, $start, (int) strpos($html, $to, $start) - $start + strlen($to));
}
private static function html(CompatibilityTab $tab): string
{
ob_start();
@@ -181,7 +189,166 @@ final class CompatibilityTabTest extends TestCase
self::assertInstanceOf(AdminTab::class, $tab);
self::assertSame('compatibility', $tab->slug());
self::assertSame('Compatibility', $tab->label());
self::assertSame('Kompatibilität', $tab->label());
}
/** @param list<Profile> $profiles */
private function tabOver(array $profiles): CompatibilityTab
{
$reports = Mockery::mock(CheckReportStore::class);
$reports->allows('latest')->andReturn(null);
$store = Mockery::mock(ReleaseStore::class);
$store->allows('entries')->andReturn([]);
$store->allows('find')->andReturn(null);
return new CompatibilityTab(
new Compatibility($profiles),
FakeChangeLog::forbidden(),
new AdminPostAction(static function (): void {
}),
static fn (): CheckReportStore => $reports,
static fn (): ReleaseStore => $store
);
}
/** A second profile; $capability keeps the capability IDs unique across profiles. */
private static function other(
string $id,
bool $active,
Tier $tier = Tier::LockOnFailure,
FakeCapability $capability = FakeCapability::Write
): Profile {
return new Profile(
id: $id,
label: ucfirst($id),
tier: $tier,
capabilities: [$capability],
active: static fn (): bool => $active,
version: static fn (): ?string => '2.0.0',
matrix: $tier === Tier::ReleasePerVersion
? static fn (): CompatibilityMatrix => CompatibilityMatrix::uniform([$capability], ReleaseState::Released, [], null)
: null
);
}
public function testTheOverviewListsEveryProfileAndLinksToTheSectionsOfTheActiveOnes(): void
{
$html = self::html($this->tabOver([
self::other('alpha', true),
self::other('beta', false, capability: FakeCapability::Other),
self::other('gamma', true, capability: FakeCapability::Read),
]));
self::assertStringContainsString('<h2>Übersicht</h2>', $html);
foreach (['Alpha', 'Beta', 'Gamma'] as $label) {
self::assertStringContainsString('data-label="Plugin">' . $label . '</th>', $html);
}
self::assertStringContainsString('href="#lit-mcp-compat-alpha"', $html);
self::assertStringContainsString('href="#lit-mcp-compat-gamma"', $html);
self::assertStringNotContainsString('#lit-mcp-compat-beta', $html);
self::assertStringContainsString('id="lit-mcp-compat-alpha"', $html);
self::assertStringNotContainsString('id="lit-mcp-compat-beta"', $html);
self::assertSame(1, substr_count($html, '<strong>nicht aktiv</strong>'));
// A link target in the page for every link, and the link names its plugin after the visible word.
self::assertStringContainsString('>Details<span class="screen-reader-text"> zu Alpha</span></a>', $html);
}
public function testTheOverviewSaysAtOnceWhetherEverythingIsFine(): void
{
$fine = self::html($this->tabOver([self::other('alpha', true)]));
$attention = self::html($this->tabOver([
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested)),
]));
self::assertStringContainsString('notice-success inline', $fine);
self::assertStringContainsString('Alles in Ordnung', $fine);
self::assertStringContainsString('notice-warning inline', $attention);
self::assertStringContainsString('1 aktives Plugin braucht deine Aufmerksamkeit.', $attention);
}
public function testTheOverviewTableIsAccessibleAndResponsive(): void
{
$html = self::html($this->tabOver([self::other('alpha', true)]));
$table = self::between($html, 'lit-mcp-compat-overview', '</tbody></table>');
self::assertStringContainsString('<caption class="screen-reader-text">', $table);
self::assertSame(5, substr_count($table, '<th scope="col">'));
self::assertStringContainsString('<th scope="row" data-label="Plugin">Alpha</th>', $table);
foreach (['Version', 'Zustand', 'Freigabe', 'Details'] as $label) {
self::assertStringContainsString('<td data-label="' . $label . '">', $table);
}
self::assertStringContainsString('Sperrt automatisch bei Problemen', $table);
}
public function testTierNamesNeverReachTheScreen(): void
{
foreach ([Tier::ReleasePerVersion, Tier::LockOnFailure, Tier::ReportOnly] as $tier) {
$html = self::html($this->tabOver([self::other('alpha', true, $tier)]));
foreach (['LockOnFailure', 'ReleasePerVersion', 'ReportOnly'] as $raw) {
self::assertStringNotContainsString($raw, $html);
}
}
}
public function testASectionShowsStateThenActionsThenDetails(): void
{
$html = self::html($this->tab(
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested)),
self::report()
));
$section = substr($html, (int) strpos($html, '<section'));
self::assertStringContainsString('aria-labelledby="lit-mcp-compat-fake-title"', $section);
self::assertStringContainsString('<h2 id="lit-mcp-compat-fake-title">Fake &lt;Plugin&gt;</h2>', $section);
$order = array_map(
static fn (string $needle): int => (int) strpos($section, $needle),
['notice notice-warning inline', '<h3>Aktionen</h3>', '<details', '<h3>Fähigkeiten</h3>', '<h3>Versionen</h3>', '<h3>Letzte Prüfung</h3>']
);
$sorted = $order;
sort($sorted);
self::assertSame($sorted, $order);
self::assertNotContains(0, $order);
}
public function testStateAndNextStepAreWordsAndNeverOnlyColour(): void
{
foreach (self::states() as [$state, $word]) {
$html = self::html($this->tab(self::profile(Tier::ReleasePerVersion, self::matrix($state))));
// overview row, section notice and one row per capability: the word beside an icon hidden from AT
$expected = 2 + count(self::CAPS);
self::assertSame($expected, substr_count($html, '<strong>' . $word . '</strong>'));
self::assertSame($expected, substr_count($html, 'aria-hidden="true"></span> <strong>' . $word . '</strong>'));
}
}
public function testTheFeatureTableNamesEachRowByItsCapability(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString('<th scope="row" data-label="Fähigkeit">Fake fake-read</th>', $html);
self::assertStringContainsString('<td data-label="Zustand">', $html);
}
public function testTheDetailsSummaryNamesThePlugin(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString(
'<summary>Details<span class="screen-reader-text"> zu Fake &lt;Plugin&gt;</span></summary>',
$html
);
}
public function testProblemsStandInTheStateNoticeEscaped(): void
{
$html = self::html($this->tab(
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Incompatible, ['Problem <A>']))
));
self::assertStringContainsString('notice notice-error inline', $html);
self::assertStringContainsString('<li>Problem &lt;A&gt;</li>', $html);
}
public function testAnInactiveProfileIsOneLineAndNeverAsksForTheMatrix(): void
@@ -190,10 +357,13 @@ final class CompatibilityTabTest extends TestCase
// ReleasePerVersion with a matrix closure that fails the test when called.
$html = self::html($this->tab(self::profile(Tier::ReleasePerVersion, null, active: false)));
self::assertStringContainsString('nicht installiert oder nicht aktiv', $html);
self::assertStringContainsString('Fake &lt;Plugin&gt;', $html);
self::assertStringContainsString('<strong>nicht aktiv</strong>', $html);
self::assertStringContainsString('data-label="Plugin">Fake &lt;Plugin&gt;</th>', $html);
self::assertStringContainsString('Kein unterstütztes Plugin ist aktiv', $html);
self::assertStringNotContainsString('<section', $html);
self::assertStringNotContainsString('<form', $html);
self::assertStringNotContainsString('<table', $html);
self::assertStringNotContainsString('href="#lit-mcp-compat-fake"', $html);
self::assertSame(1, substr_count($html, '<table'));
}
/** @dataProvider states */
@@ -243,11 +413,8 @@ final class CompatibilityTabTest extends TestCase
{
return [
'release' => [Tier::ReleasePerVersion, 'Freigabe pro Version nötig'],
'lock' => [
Tier::LockOnFailure,
'Freigabe wird dokumentiert, Nutzung automatisch, solange die Prüfung grün ist',
],
'report' => [Tier::ReportOnly, 'nur Anzeige'],
'lock' => [Tier::LockOnFailure, 'Sperrt automatisch bei Problemen'],
'report' => [Tier::ReportOnly, 'Nur Anzeige'],
];
}
@@ -289,8 +456,8 @@ final class CompatibilityTabTest extends TestCase
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString('Getestete Versionen', $html);
self::assertStringContainsString('Freigegebene Versionen', $html);
self::assertStringContainsString('<th scope="row">Im Plugin getestet</th>', $html);
self::assertStringContainsString('<th scope="row">Von dir freigegeben</th>', $html);
self::assertSame(2, substr_count($html, '<td>–</td>'));
}
@@ -492,7 +659,7 @@ final class CompatibilityTabTest extends TestCase
$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]'));
self::assertGreaterThan(strpos($html, '</details>'), strpos($html, '[EXTRA]'));
}
public function testAnInactiveProfileFiresNeitherIntroNorExtra(): void
@@ -542,7 +709,7 @@ final class CompatibilityTabTest extends TestCase
$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::assertSame(3, substr_count(self::between($html, 'lit-mcp-compat-features', '</thead>'), '<th scope="col">'));
self::assertStringContainsString('<td data-label="Gut"><ul><li>ok</li></ul></td>', $html);
}
@@ -567,7 +734,7 @@ final class CompatibilityTabTest extends TestCase
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('</tbody></table><h3>Versionen</h3>', $html);
self::assertStringContainsString('fake capability column "Kaputt" failed', $logged);
self::assertStringContainsString('Zelle <kaputt>', $logged);
}
@@ -591,7 +758,10 @@ final class CompatibilityTabTest extends TestCase
{
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn('kaputt');
self::assertSame(2, substr_count(self::html($this->tab(self::profile())), '<th scope="col">'));
self::assertSame(
2,
substr_count(self::between(self::html($this->tab(self::profile())), 'lit-mcp-compat-features', '</thead>'), '<th scope="col">')
);
}
public function testEverythingFromThirdPartiesIsEscaped(): void
+1 -1
View File
@@ -278,7 +278,7 @@ final class SettingsPageTest extends TestCase
static fn (array $tab): bool => $tab['id'] === 'lit-mcp-overview'
&& str_contains($tab['content'], 'Tab „Verbindung“')
&& str_contains($tab['content'], 'Tab „Tools“')
&& str_contains($tab['content'], 'Tab „Compatibility“')
&& str_contains($tab['content'], 'Tab „Kompatibilität“')
));
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->helpTabFor($screen);
@@ -132,7 +132,7 @@ final class DiviAdminExtrasTest extends TestCase
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);
self::assertStringContainsString('Reiter „Kompatibilität“', $html);
}
public function testNoNoticeWhileEveryFeatureIsUsable(): void
@@ -77,7 +77,7 @@ final class ReleaseStatusTest extends TestCase
self::assertSame(
'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 → Compatibility die Prüfung ausführt '
. 'Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Kompatibilität die Prüfung ausführt '
. 'und die Version freigibt. Es wurde nichts geändert.',
$payload['message']
);
+4 -2
View File
@@ -172,8 +172,10 @@ final class PluginTest extends TestCase
self::assertInstanceOf(CompatibilityTab::class, $tab);
self::assertSame('compatibility', $tab->slug());
self::assertStringContainsString('id="lit-mcp-compat-divi"', $html);
self::assertStringContainsString('id="lit-mcp-compat-pods"', $html);
// Both plugins are inactive in this registry: one overview row each, no section.
self::assertStringContainsString('data-label="Plugin">Divi</th>', $html);
self::assertStringContainsString('data-label="Plugin">Pods</th>', $html);
self::assertStringNotContainsString('<section', $html);
}
private static function readOnly(object $source): bool