The nonce actions check_admin_referer() was asked for. */ private array $nonces = []; protected function setUp(): void { parent::setUp(); $this->redirect = null; $this->nonces = []; Functions\stubEscapeFunctions(); Functions\stubTranslationFunctions(); Functions\when('current_user_can')->justReturn(true); Functions\when('check_admin_referer')->alias(function (string $action): int { $this->nonces[] = $action; return 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('rest_url')->alias(static fn (string $path): string => 'https://k.de/wp-json/' . $path); Functions\when('wp_create_nonce')->alias(static fn (string $action): string => 'nonce-' . $action); Functions\when('wp_nonce_field')->alias(static function (string $action): void { echo ''; }); Functions\when('submit_button')->alias( static function ( string $text, string $type = 'primary', string $name = 'submit', bool $wrap = true, array $attributes = [] ): void { $extra = ''; foreach ($attributes as $key => $value) { $extra .= ' ' . $key . '="' . esc_attr($value) . '"'; // WordPress escapes the values itself } echo ''; } ); Functions\when('get_option')->alias(static fn (string $name): string => $name === 'date_format' ? 'Y-m-d' : ''); Functions\when('date_i18n')->alias(static fn (string $format, int $time): string => gmdate($format, $time)); Functions\when('get_userdata')->justReturn(false); Functions\when('sanitize_key')->alias( static fn (string $key): string => strtolower((string) preg_replace('/[^a-z0-9_\-]/i', '', $key)) ); Functions\when('wp_unslash')->returnArg(); } protected function tearDown(): void { unset($_POST['profile'], $_GET['tab']); parent::tearDown(); } /** A profile of the invented plugin "fake"; $matrix only for ReleasePerVersion. */ private static function profile( Tier $tier = Tier::LockOnFailure, ?CompatibilityMatrix $matrix = null, bool $active = true, ?string $version = '1.2.0', array $tested = [], ?Closure $stepLabel = null ): Profile { return new Profile( id: 'fake', label: 'Fake ', tier: $tier, capabilities: self::CAPS, active: static fn (): bool => $active, version: static fn (): ?string => $version, matrix: $tier === Tier::ReleasePerVersion ? static fn (): CompatibilityMatrix => $matrix ?? self::fail('no matrix expected') : null, testedVersions: $tested, stateKey: static fn (string $v): string => 'key' . $v, stepLabel: $stepLabel ); } private static function matrix(ReleaseState $state, array $problems = []): CompatibilityMatrix { return CompatibilityMatrix::uniform(self::CAPS, $state, $problems, null); } private static function report(bool $green = true, string $version = '1.2.0', string $key = 'key1.2.0'): CheckReport { $steps = [ new CheckStep(CheckStep::MANIFEST, $green ? [] : ['Funktion x fehlt'], false, 'Manifest Label'), new CheckStep('old', [], false), ]; return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, $version, Plugin::VERSION, $key, $steps); } private function tab( Profile $profile, ?CheckReport $report = null, ?ReleaseStore $releases = null, ?FakeChangeLog $log = null ): CompatibilityTab { $reports = Mockery::mock(CheckReportStore::class); $reports->allows('latest')->andReturn($report); $store = $releases ?? Mockery::mock(ReleaseStore::class); if ($releases === null) { $store->allows('entries')->andReturn([]); $store->allows('find')->andReturn(null); } return new CompatibilityTab( new Compatibility([$profile]), $log ?? FakeChangeLog::forbidden(), new AdminPostAction(static function (): void { }), static fn (): CheckReportStore => $reports, static fn (): ReleaseStore => $store ); } private static function html(CompatibilityTab $tab): string { ob_start(); $tab->render(); return (string) ob_get_clean(); } public function testItIsTheTabCompatibility(): void { $tab = $this->tab(self::profile()); self::assertInstanceOf(AdminTab::class, $tab); self::assertSame('compatibility', $tab->slug()); self::assertSame('Compatibility', $tab->label()); } 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 installiert oder nicht aktiv', $html); self::assertStringContainsString('Fake <Plugin>', $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, 'Freigabe wird dokumentiert, Nutzung automatisch, solange die Prüfung grün ist', ], '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('Getestete Versionen', $html); self::assertStringContainsString('Freigegebene Versionen', $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('
  • <i>a</i>
', $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($html, '')); self::assertStringContainsString('
    • ok
    ', $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('
    • ok
    ', $html); self::assertStringContainsString('
    ', $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::html($this->tab(self::profile())), '')); } 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('