', $table);
self::assertSame(5, substr_count($table, '| '));
self::assertStringContainsString(' | Alpha | ', $table);
foreach (['Version', 'Zustand', 'Freigabe', 'Details'] as $label) {
self::assertStringContainsString('', $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, 'Fake <Plugin>', $section);
$order = array_map(
static fn (string $needle): int => (int) strpos($section, $needle),
['notice notice-warning inline', 'Aktionen', 'Fähigkeiten', 'Versionen', 'Letzte Prüfung']
);
$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, '' . $word . ''));
self::assertSame($expected, substr_count($html, 'aria-hidden="true"> ' . $word . ''));
}
}
public function testTheFeatureTableNamesEachRowByItsCapability(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString('Fake fake-read | ', $html);
self::assertStringContainsString('', $html);
}
public function testTheDetailsSummaryNamesThePlugin(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString(
'Details zu Fake <Plugin>',
$html
);
}
public function testProblemsStandInTheStateNoticeEscaped(): void
{
$html = self::html($this->tab(
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Incompatible, ['Problem ']))
));
self::assertStringContainsString('notice notice-error inline', $html);
self::assertStringContainsString('Problem <A>', $html);
}
public function testAnInactiveProfileIsOneLineAndNeverAsksForTheMatrix(): void
{
Actions\expectDone('lit_mcp_compat_profile_extra')->never();
// 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 aktiv', $html);
self::assertStringContainsString('data-label="Plugin">Fake <Plugin>', $html);
self::assertStringContainsString('Kein unterstütztes Plugin ist aktiv', $html);
self::assertStringNotContainsString('tab(self::profile(Tier::ReleasePerVersion, self::matrix($state, ['Problem A']))));
self::assertStringContainsString('' . $word . '', $html);
self::assertStringContainsString('dashicons-' . $icon, $html);
self::assertStringContainsString('aria-hidden="true"', $html);
}
/** @return array */
public static function states(): array
{
return [
'released' => [ReleaseState::Released, 'freigegeben', 'yes-alt'],
'untested' => [ReleaseState::Untested, 'ungetestet', 'warning'],
'incompatible' => [ReleaseState::Incompatible, 'inkompatibel', 'dismiss'],
];
}
public function testItShowsTheVersionAndTheProblemsAndTheCapabilityTable(): void
{
$html = self::html($this->tab(
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Incompatible, ['Problem A']))
));
self::assertStringContainsString('1.2.0', $html);
self::assertStringContainsString('Problem A', $html);
self::assertStringContainsString('Fake fake-read', $html);
self::assertStringContainsString('Fake fake-write', $html);
}
/** @dataProvider tierTexts */
public function testTheTierHintSaysWhatTheReleaseMeans(Tier $tier, string $text): void
{
$matrix = $tier === Tier::ReleasePerVersion ? self::matrix(ReleaseState::Released) : null;
$html = self::html($this->tab(self::profile($tier, $matrix)));
self::assertStringContainsString($text, $html);
}
/** @return array */
public static function tierTexts(): array
{
return [
'release' => [Tier::ReleasePerVersion, 'Freigabe pro Version nötig'],
'lock' => [Tier::LockOnFailure, 'Sperrt automatisch bei Problemen'],
'report' => [Tier::ReportOnly, 'Nur Anzeige'],
];
}
public function testLockOnFailureIsLockedWhenTheProfileIsActiveButTooOld(): void
{
$profile = new Profile(
id: 'fake',
label: 'Fake',
tier: Tier::LockOnFailure,
capabilities: self::CAPS,
version: static fn (): string => '0.9',
minVersion: '1.0'
);
$tab = new CompatibilityTab(new Compatibility([$profile]), FakeChangeLog::forbidden());
Functions\when('get_option')->justReturn(false);
$html = self::html($tab);
self::assertStringContainsString('inkompatibel', $html);
self::assertStringContainsString('Mindestversion', $html);
}
public function testTestedAndReleasedVersionsAreListed(): void
{
$releases = Mockery::mock(ReleaseStore::class);
$releases->allows('entries')->andReturn([
['version' => '1.1.0', 'plugin_version' => '0.7.0', 'user_id' => 1, 'released_at' => 'x', 'report_id' => 'r'],
]);
$releases->allows('find')->andReturn(null);
$html = self::html($this->tab(self::profile(tested: ['1.0.0', '1.0.1']), null, $releases));
self::assertStringContainsString('1.0.0, 1.0.1', $html);
self::assertStringContainsString('1.1.0', $html);
self::assertStringContainsString('LIT MCP 0.7.0', $html);
}
public function testNoVersionListsShowADashWhenEmpty(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString('Im Plugin getestet | ', $html);
self::assertStringContainsString('Von dir freigegeben | ', $html);
self::assertSame(2, substr_count($html, '– | '));
}
public function testTheLatestReportListsEveryStepWithIconAndLabel(): void
{
$profile = self::profile(stepLabel: static fn (CheckStep $s): string => $s->label ?? 'Alt:' . $s->key);
$html = self::html($this->tab($profile, self::report()));
self::assertStringContainsString('Letzte Prüfung', $html);
self::assertStringContainsString('Manifest <b>Label</b>', $html);
self::assertStringContainsString('Alt:old', $html);
self::assertStringContainsString('bestanden', $html);
}
public function testAFailedStepSaysSoInVisibleText(): void
{
$html = self::html($this->tab(self::profile(), self::report(false)));
self::assertStringContainsString('fehlgeschlagen', $html);
}
public function testTheCapabilityTableHasACaptionNamingTheProfile(): void
{
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString(
'Fähigkeiten von Fake <Plugin>',
$html
);
}
public function testAProfileCanAddColumnsToTheCapabilityTableAndTheTabEscapesThem(): void
{
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->with([], 'fake')->andReturn([
[
'heading' => 'Quelle ',
'cell' => static fn (FakeCapability $c, CompatibilityMatrix $m): array => $c === FakeCapability::Read
? ['a']
: [],
],
[
'heading' => 'Tools',
'cell' => static fn (FakeCapability $c, CompatibilityMatrix $m): array => ['tool-' . $c->id()],
'code' => true,
],
]);
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString('Quelle <b> | ', $html);
self::assertStringContainsString(' | ', $html);
self::assertStringContainsString('– | ', $html);
self::assertStringContainsString('tool-' . FakeCapability::Read->id() . '', $html);
self::assertStringNotContainsString('a', $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('', $released);
self::assertStringContainsString('', $untested);
self::assertStringContainsString('lit-mcp-compat-features', $released);
}
public function testEveryButtonNamesItsProfileForAssistiveTechnology(): void
{
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
$withdrawable = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released));
$html = self::html($this->tab($profile, self::report()))
. self::html($this->tab($withdrawable, self::report(), self::store(self::entry())));
self::assertStringContainsString('aria-label="Prüfung starten: Fake <Plugin>"', $html);
// The release label names the profile itself.
self::assertStringContainsString('>Fake 1.2.0 freigeben', $html);
self::assertStringContainsString('aria-label="Freigabe zurückziehen: Fake <Plugin>"', $html);
}
public function testEachFormButtonHasAUniqueNameInsteadOfSubmit(): void
{
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
$withdrawable = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released));
$html = self::html($this->tab($profile, self::report()))
. self::html($this->tab($withdrawable, self::report(), self::store(self::entry())));
self::assertStringNotContainsString('name="submit"', $html);
self::assertStringContainsString('name="lit-mcp-compat-release-fake"', $html);
self::assertStringContainsString('name="lit-mcp-compat-withdraw-fake"', $html);
self::assertStringContainsString('name="lit-mcp-compat-check-fake"', $html);
}
public function testTheLoadHookIsRegisteredOnTheSettingsPage(): void
{
$tab = $this->tab(self::profile());
Actions\expectAdded('load-settings_page_lit-mcp')->once()->with([$tab, 'loadProfiles']);
$tab->registerActions();
}
public function testTheLoadActionFiresPerProfileOnTheOwnTabOnly(): void
{
$_GET['tab'] = 'compatibility';
Actions\expectDone('lit_mcp_compat_profile_load')->once()->with('fake');
$this->tab(self::profile())->loadProfiles();
}
public function testTheLoadActionStaysQuietOnOtherTabsAndForOthersThanAdministrators(): void
{
Actions\expectDone('lit_mcp_compat_profile_load')->never();
foreach (['verbindung', '', ['compatibility']] as $tabParam) {
$_GET['tab'] = $tabParam;
$this->tab(self::profile())->loadProfiles();
}
unset($_GET['tab']);
$this->tab(self::profile())->loadProfiles();
$_GET['tab'] = 'compatibility';
Functions\when('current_user_can')->justReturn(false);
$this->tab(self::profile())->loadProfiles();
}
public function testRenderingDoesNotFireTheLoadAction(): void
{
$_GET['tab'] = 'compatibility';
Actions\expectDone('lit_mcp_compat_profile_load')->never();
self::html($this->tab(self::profile()));
}
public function testAnArrayAsProfileFieldIsRefusedWithoutAWarning(): void
{
$this->actionsDie();
$_POST['profile'] = ['fake'];
$this->expectException(\RuntimeException::class);
$this->tab(self::profile())->release();
}
public function testASkippedStepIsMarkedAsNotRun(): void
{
$report = new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, '1.2.0', Plugin::VERSION, 'k', [
new CheckStep('write', [], true, 'Schreibtest'),
]);
$html = self::html($this->tab(self::profile(), $report));
self::assertStringContainsString('Schreibtest', $html);
self::assertStringContainsString('übersprungen', $html);
self::assertStringNotContainsString('screen-reader-text">übersprungen', $html);
self::assertStringContainsString('nicht ausgeführt', $html);
}
public function testAStepWithoutLabelAndWithoutLabelSourceShowsItsKey(): void
{
$html = self::html($this->tab(self::profile(), self::report()));
self::assertStringContainsString(': old', $html);
}
public function testAFailedStepListsItsProblems(): void
{
$html = self::html($this->tab(self::profile(), self::report(false)));
self::assertStringContainsString('fehlgeschlagen', $html);
self::assertStringContainsString('Funktion <i>x</i> fehlt', $html);
}
public function testWithoutAReportNothingIsListed(): void
{
self::assertStringNotContainsString('Letzte Prüfung', self::html($this->tab(self::profile())));
}
public function testTheProfileExtraHookRunsLastWithTheProfileId(): void
{
Actions\expectDone('lit_mcp_compat_profile_extra')->once()->with('fake');
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, ' '), 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 Text');
$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('Gut | ', $html);
self::assertSame(3, substr_count(self::between($html, 'lit-mcp-compat-features', ''), ''));
self::assertStringContainsString(' | | ', $html);
}
public function testAThrowingCellShowsADashLogsTheFailureAndKeepsTheTableClosed(): void
{
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn([
self::column('Kaputt', static function (): array {
throw new \RuntimeException('Zelle ');
}),
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('– | ', $html);
self::assertStringContainsString(' | ', $html);
self::assertStringContainsString('Versionen', $html);
self::assertStringContainsString('fake capability column "Kaputt" failed', $logged);
self::assertStringContainsString('Zelle ', $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('– | ', $html);
self::assertStringContainsString('– | ', $html);
self::assertStringContainsString('– | ', $html);
}
public function testANonArrayColumnsFilterResultMeansNoExtraColumns(): void
{
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn('kaputt');
self::assertSame(
2,
substr_count(self::between(self::html($this->tab(self::profile())), 'lit-mcp-compat-features', ''), '')
);
}
public function testEverythingFromThirdPartiesIsEscaped(): void
{
$profile = self::profile(
Tier::ReleasePerVersion,
self::matrix(ReleaseState::Incompatible, ['']),
version: 'x',
tested: ['t']
);
$html = self::html($this->tab($profile, self::report(false, 'x')));
self::assertStringNotContainsString(' | | |