Die Cache-Integration war für WP Super Cache gebaut, eingesetzt wird aber WP Fastest Cache. Profil (wpfc), Listener, Drosselung, lit/cache-clear und Leeren-Probe laufen jetzt über die öffentlichen WPFC-Funktionen wpfc_clear_post_cache_by_id/wpfc_clear_all_cache; die Probe leert nur die Wegwerf-Seite (singleDeleteCache ohne Elternseiten) und prüft, dass WPFC den Hook noch verarbeitet. Smoke fastest-cache-smoke.sh ersetzt den WPSC-Smoke. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1077 lines
45 KiB
PHP
1077 lines
45 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Admin;
|
||
|
||
use Brain\Monkey\Actions;
|
||
use Brain\Monkey\Filters;
|
||
use Brain\Monkey\Functions;
|
||
use Closure;
|
||
use LIT\MCP\Admin\AdminPostAction;
|
||
use LIT\MCP\Admin\AdminTab;
|
||
use LIT\MCP\Admin\CompatibilityTab;
|
||
use LIT\MCP\ChangeLog\ChangeLog;
|
||
use LIT\MCP\Compatibility\CheckReport;
|
||
use LIT\MCP\Compatibility\CheckReportStore;
|
||
use LIT\MCP\Compatibility\CheckStep;
|
||
use LIT\MCP\Compatibility\Compatibility;
|
||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||
use LIT\MCP\Compatibility\Profile;
|
||
use LIT\MCP\Compatibility\ReleaseState;
|
||
use LIT\MCP\Compatibility\ReleaseStore;
|
||
use LIT\MCP\Compatibility\Tier;
|
||
use LIT\MCP\Plugin;
|
||
use LIT\MCP\Tests\Support\FakeCapability;
|
||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||
use LIT\MCP\Tests\Unit\TestCase;
|
||
use Mockery;
|
||
|
||
final class CompatibilityTabTest extends TestCase
|
||
{
|
||
private const CAPS = [FakeCapability::Read, FakeCapability::Write];
|
||
|
||
private ?string $redirect = null;
|
||
/** @var list<string> 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 '<input type="hidden" name="_wpnonce" value="nonce-' . $action . '">';
|
||
});
|
||
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 '<button class="button-' . $type . '" name="' . $name . '"' . $extra . '>' . $text . '</button>';
|
||
}
|
||
);
|
||
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 <Plugin>',
|
||
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 <i>x</i> fehlt'], false, 'Manifest <b>Label</b>'),
|
||
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
|
||
);
|
||
}
|
||
|
||
/** 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();
|
||
$tab->render();
|
||
|
||
return (string) ob_get_clean();
|
||
}
|
||
|
||
/** One source for the ability name of the release and withdraw entries (FastestCacheListener reads it too). */
|
||
public function testTheReleaseAbilityNameHasOneSource(): void
|
||
{
|
||
self::assertSame('lit/divi-release', CompatibilityTab::releaseAbility('divi'));
|
||
self::assertTrue(CompatibilityTab::isReleaseAbility(CompatibilityTab::releaseAbility('wpfc')));
|
||
foreach (['lit/content-update', 'lit/-release', 'lit/release', 'lit/divi-releases'] as $other) {
|
||
self::assertFalse(CompatibilityTab::isReleaseAbility($other), $other);
|
||
}
|
||
}
|
||
|
||
public function testItIsTheTabCompatibility(): void
|
||
{
|
||
$tab = $this->tab(self::profile());
|
||
|
||
self::assertInstanceOf(AdminTab::class, $tab);
|
||
self::assertSame('compatibility', $tab->slug());
|
||
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 <Plugin></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 <Plugin></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 <A></li>', $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('<strong>nicht aktiv</strong>', $html);
|
||
self::assertStringContainsString('data-label="Plugin">Fake <Plugin></th>', $html);
|
||
self::assertStringContainsString('Kein unterstütztes Plugin ist aktiv', $html);
|
||
self::assertStringNotContainsString('<section', $html);
|
||
self::assertStringNotContainsString('<form', $html);
|
||
self::assertStringNotContainsString('href="#lit-mcp-compat-fake"', $html);
|
||
self::assertSame(1, substr_count($html, '<table'));
|
||
}
|
||
|
||
/** @dataProvider states */
|
||
public function testTheOverallStateIsShownAsWordAndIcon(ReleaseState $state, string $word, string $icon): void
|
||
{
|
||
$html = self::html($this->tab(self::profile(Tier::ReleasePerVersion, self::matrix($state, ['Problem A']))));
|
||
|
||
self::assertStringContainsString('<strong>' . $word . '</strong>', $html);
|
||
self::assertStringContainsString('dashicons-' . $icon, $html);
|
||
self::assertStringContainsString('aria-hidden="true"', $html);
|
||
}
|
||
|
||
/** @return array<string, array{ReleaseState, string, string}> */
|
||
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<string, array{Tier, string}> */
|
||
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('<strong>inkompatibel</strong>', $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('<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>'));
|
||
}
|
||
|
||
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('<strong>bestanden</strong>', $html);
|
||
}
|
||
|
||
public function testAFailedStepSaysSoInVisibleText(): void
|
||
{
|
||
$html = self::html($this->tab(self::profile(), self::report(false)));
|
||
|
||
self::assertStringContainsString('<strong>fehlgeschlagen</strong>', $html);
|
||
}
|
||
|
||
public function testTheCapabilityTableHasACaptionNamingTheProfile(): void
|
||
{
|
||
$html = self::html($this->tab(self::profile()));
|
||
|
||
self::assertStringContainsString(
|
||
'<caption class="screen-reader-text">Fähigkeiten von Fake <Plugin></caption>',
|
||
$html
|
||
);
|
||
}
|
||
|
||
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
|
||
{
|
||
$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 <Plugin> 1.2.0 freigeben</button>', $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('<strong>übersprungen</strong>', $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('</strong>: 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, '</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(self::between($html, 'lit-mcp-compat-features', '</thead>'), '<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><h3>Versionen</h3>', $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::between(self::html($this->tab(self::profile())), 'lit-mcp-compat-features', '</thead>'), '<th scope="col">')
|
||
);
|
||
}
|
||
|
||
public function testEverythingFromThirdPartiesIsEscaped(): void
|
||
{
|
||
$profile = self::profile(
|
||
Tier::ReleasePerVersion,
|
||
self::matrix(ReleaseState::Incompatible, ['<script>alert(1)</script>']),
|
||
version: '<b>x</b>',
|
||
tested: ['<i>t</i>']
|
||
);
|
||
|
||
$html = self::html($this->tab($profile, self::report(false, '<b>x</b>')));
|
||
|
||
self::assertStringNotContainsString('<script>', $html);
|
||
self::assertStringNotContainsString('<b>x</b>', $html);
|
||
self::assertStringNotContainsString('<i>t</i>', $html);
|
||
self::assertStringNotContainsString('<i>x</i>', $html);
|
||
self::assertStringNotContainsString('<b>Label</b>', $html);
|
||
self::assertStringContainsString('<b>x</b>', $html);
|
||
self::assertStringContainsString('<script>alert(1)</script>', $html);
|
||
}
|
||
|
||
// --- 1.21: forms and actions -------------------------------------------------------------------------------
|
||
|
||
private function actionsDie(): void
|
||
{
|
||
Functions\when('wp_die')->alias(static function (): never {
|
||
throw new \RuntimeException('died');
|
||
});
|
||
}
|
||
|
||
private static function entry(string $version = '1.2.0'): array
|
||
{
|
||
return [
|
||
'version' => $version,
|
||
'plugin_version' => Plugin::VERSION,
|
||
'user_id' => 1,
|
||
'released_at' => '2026-10-01T08:00:00+00:00',
|
||
'report_id' => 'r1',
|
||
];
|
||
}
|
||
|
||
/** A store with the given entries for the installed state; add()/withdraw() expectations are the test's. */
|
||
private static function store(?array $entry = null): ReleaseStore
|
||
{
|
||
$releases = Mockery::mock(ReleaseStore::class);
|
||
$releases->allows('entries')->andReturn($entry === null ? [] : [$entry]);
|
||
$releases->allows('find')->andReturn($entry);
|
||
|
||
return $releases;
|
||
}
|
||
|
||
public function testRegistersTheAdminPostHandlers(): void
|
||
{
|
||
$tab = $this->tab(self::profile());
|
||
Actions\expectAdded('admin_post_lit_mcp_compat_release')->once()->with([$tab, 'release']);
|
||
Actions\expectAdded('admin_post_lit_mcp_compat_withdraw')->once()->with([$tab, 'withdraw']);
|
||
|
||
$tab->registerActions();
|
||
}
|
||
|
||
public function testTheCheckFormPostsToTheRestRouteWithTheRestNonce(): void
|
||
{
|
||
$html = self::html($this->tab(self::profile()));
|
||
|
||
self::assertStringContainsString('action="https://k.de/wp-json/lit-mcp/v1/fake-check"', $html);
|
||
self::assertStringContainsString('name="_wpnonce" value="nonce-wp_rest"', $html);
|
||
self::assertStringContainsString('Prüfung starten', $html);
|
||
}
|
||
|
||
public function testReleaseNeedsAFreshGreenReportForTheInstalledState(): void
|
||
{
|
||
$_POST['profile'] = 'fake';
|
||
$reports = [null, self::report(false), self::report(true, '1.1.0', 'key1.1.0'), self::report(true, '1.2.0', 'old')];
|
||
foreach ($reports as $report) {
|
||
$this->redirect = null;
|
||
$releases = self::store();
|
||
$releases->expects('add')->never();
|
||
Actions\expectDone('lit_mcp_compat_changed')->never();
|
||
|
||
$this->tab(self::profile(), $report, $releases)->release();
|
||
|
||
self::assertStringContainsString('lit_mcp_notice=compat_release_refused', (string) $this->redirect);
|
||
self::assertStringContainsString('lit_mcp_profile=fake', (string) $this->redirect);
|
||
}
|
||
}
|
||
|
||
public function testReleaseStoresTheEntryLogsItAndAnnouncesTheChange(): void
|
||
{
|
||
$_POST['profile'] = 'fake';
|
||
$releases = self::store();
|
||
$releases->expects('add')->once()->with('1.2.0', Plugin::VERSION, 1, 'r1');
|
||
$log = new FakeChangeLog();
|
||
Actions\expectDone('lit_mcp_compat_changed')->once()->with('fake');
|
||
|
||
$this->tab(self::profile(), self::report(), $releases, $log)->release();
|
||
|
||
self::assertSame(['lit_mcp_compat_release_fake'], $this->nonces);
|
||
self::assertStringContainsString('tab=compatibility', (string) $this->redirect);
|
||
self::assertStringContainsString('lit_mcp_notice=compat_released', (string) $this->redirect);
|
||
self::assertSame('lit/fake-release', $log->records[0]['ability']);
|
||
self::assertSame('fake', $log->records[0]['object_type']);
|
||
self::assertSame('1.2.0', $log->records[0]['object_id']);
|
||
self::assertStringContainsString('Fake <Plugin> 1.2.0', $log->records[0]['summary']);
|
||
}
|
||
|
||
/** @dataProvider refusedReleases */
|
||
public function testReleaseIsRefusedWhereTheTabOffersNone(Profile $profile, ?array $entry): void
|
||
{
|
||
$_POST['profile'] = 'fake';
|
||
$releases = self::store($entry);
|
||
$releases->expects('add')->never();
|
||
Actions\expectDone('lit_mcp_compat_changed')->never();
|
||
|
||
$this->tab($profile, self::report(), $releases)->release();
|
||
|
||
self::assertStringContainsString('lit_mcp_notice=compat_release_refused', (string) $this->redirect);
|
||
}
|
||
|
||
/** @return array<string, array{Profile, ?array}> */
|
||
public static function refusedReleases(): array
|
||
{
|
||
return [
|
||
'version tested in the plugin' => [self::profile(tested: ['1.2.0']), null],
|
||
'already released' => [self::profile(), self::entry()],
|
||
'report only' => [self::profile(Tier::ReportOnly), null],
|
||
'inactive' => [self::profile(active: false), null],
|
||
'version unknown' => [self::profile(version: null), null],
|
||
'per version, but already released' => [
|
||
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released)),
|
||
null,
|
||
],
|
||
'per version, but incompatible' => [
|
||
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Incompatible, ['x'])),
|
||
null,
|
||
],
|
||
];
|
||
}
|
||
|
||
public function testAReleasePerVersionProfileIsReleasedWhileUntested(): void
|
||
{
|
||
$_POST['profile'] = 'fake';
|
||
$releases = self::store();
|
||
$releases->expects('add')->once();
|
||
Actions\expectDone('lit_mcp_compat_changed')->once();
|
||
|
||
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
|
||
|
||
$this->tab($profile, self::report(), $releases, new FakeChangeLog())->release();
|
||
|
||
self::assertStringContainsString('lit_mcp_notice=compat_released', (string) $this->redirect);
|
||
}
|
||
|
||
public function testAnUnknownProfileIsRefusedBeforeTheNonceOrAnyStore(): void
|
||
{
|
||
$this->actionsDie();
|
||
Actions\expectDone('lit_mcp_compat_changed')->never();
|
||
$releases = self::store();
|
||
$releases->expects('add')->never();
|
||
$releases->expects('withdraw')->never();
|
||
|
||
foreach (['nope', '', "fake' OR 1=1"] as $id) {
|
||
$_POST['profile'] = $id;
|
||
foreach (['release', 'withdraw'] as $method) {
|
||
try {
|
||
$this->tab(self::profile(), self::report(), $releases)->$method();
|
||
self::fail('expected the request to stop');
|
||
} catch (\RuntimeException $e) {
|
||
self::assertSame('died', $e->getMessage());
|
||
}
|
||
}
|
||
}
|
||
self::assertNull($this->redirect);
|
||
self::assertSame([], $this->nonces);
|
||
}
|
||
|
||
public function testAMissingProfileFieldIsRefused(): void
|
||
{
|
||
$this->actionsDie();
|
||
unset($_POST['profile']);
|
||
|
||
$this->expectException(\RuntimeException::class);
|
||
|
||
$this->tab(self::profile())->release();
|
||
}
|
||
|
||
public function testActionsNeedManageOptionsAndTheNonceOfTheProfile(): void
|
||
{
|
||
$_POST['profile'] = 'fake';
|
||
$this->actionsDie();
|
||
Functions\when('current_user_can')->justReturn(false);
|
||
$releases = self::store();
|
||
$releases->expects('add')->never();
|
||
$releases->expects('withdraw')->never();
|
||
foreach (['release', 'withdraw'] as $method) {
|
||
try {
|
||
$this->tab(self::profile(), self::report(), $releases)->$method();
|
||
self::fail('expected 403');
|
||
} catch (\RuntimeException $e) {
|
||
self::assertSame('died', $e->getMessage());
|
||
}
|
||
}
|
||
|
||
// With the right but a failing nonce check (wrong nonce: WordPress dies) nothing is stored either.
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('check_admin_referer')->alias(static function (): never {
|
||
throw new \RuntimeException('bad nonce');
|
||
});
|
||
foreach (['release', 'withdraw'] as $method) {
|
||
try {
|
||
$this->tab(self::profile(), self::report(), $releases)->$method();
|
||
self::fail('expected the nonce to stop');
|
||
} catch (\RuntimeException $e) {
|
||
self::assertSame('bad nonce', $e->getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
public function testWithdrawRemovesTheEntryOfTheInstalledVersionLogsItAndAnnouncesTheChange(): void
|
||
{
|
||
$_POST['profile'] = 'fake';
|
||
$releases = self::store(self::entry());
|
||
$releases->expects('withdraw')->once()->with('1.2.0', Plugin::VERSION)->andReturn(true);
|
||
$log = new FakeChangeLog();
|
||
Actions\expectDone('lit_mcp_compat_changed')->once()->with('fake');
|
||
|
||
$this->tab(self::profile(), null, $releases, $log)->withdraw();
|
||
|
||
self::assertSame(['lit_mcp_compat_withdraw_fake'], $this->nonces);
|
||
self::assertStringContainsString('lit_mcp_notice=compat_withdrawn', (string) $this->redirect);
|
||
self::assertStringContainsString('lit_mcp_profile=fake', (string) $this->redirect);
|
||
self::assertSame('lit/fake-release', $log->records[0]['ability']);
|
||
}
|
||
|
||
public function testWithdrawWithoutAnEntryFails(): void
|
||
{
|
||
$_POST['profile'] = 'fake';
|
||
$releases = self::store();
|
||
$releases->expects('withdraw')->once()->andReturn(false);
|
||
Actions\expectDone('lit_mcp_compat_changed')->never();
|
||
|
||
$this->tab(self::profile(), null, $releases)->withdraw();
|
||
|
||
self::assertStringContainsString('lit_mcp_notice=compat_withdraw_failed', (string) $this->redirect);
|
||
}
|
||
|
||
public function testUntestedWithAFreshGreenReportOffersTheRelease(): void
|
||
{
|
||
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
|
||
|
||
$html = self::html($this->tab($profile, self::report()));
|
||
|
||
self::assertStringContainsString('name="action" value="lit_mcp_compat_release"', $html);
|
||
self::assertStringContainsString('name="profile" value="fake"', $html);
|
||
self::assertStringContainsString('value="nonce-lit_mcp_compat_release_fake"', $html);
|
||
self::assertStringContainsString('1.2.0 freigeben', $html);
|
||
self::assertStringNotContainsString('lit_mcp_compat_withdraw', $html);
|
||
}
|
||
|
||
public function testUntestedWithoutAFreshReportAsksForACheckInsteadOfOfferingTheRelease(): void
|
||
{
|
||
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
|
||
|
||
foreach ([null, self::report(false)] as $report) {
|
||
$html = self::html($this->tab($profile, $report));
|
||
|
||
self::assertStringNotContainsString('lit_mcp_compat_release', $html);
|
||
self::assertStringContainsString('Freigeben geht erst nach einer vollständig grünen Prüfung', $html);
|
||
}
|
||
}
|
||
|
||
public function testAReleasedPerVersionProfileOffersNeitherReleaseNorWithdrawal(): void
|
||
{
|
||
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released));
|
||
|
||
$html = self::html($this->tab($profile, self::report()));
|
||
|
||
self::assertStringNotContainsString('lit_mcp_compat_release', $html);
|
||
self::assertStringNotContainsString('lit_mcp_compat_withdraw', $html);
|
||
}
|
||
|
||
public function testLockOnFailureOffersTheReleaseForAVersionNotTestedInThePlugin(): void
|
||
{
|
||
$html = self::html($this->tab(self::profile(), self::report()));
|
||
|
||
self::assertStringContainsString('value="lit_mcp_compat_release"', $html);
|
||
self::assertStringNotContainsString('lit_mcp_compat_withdraw', $html);
|
||
}
|
||
|
||
public function testATestedVersionAndReportOnlyOfferNoRelease(): void
|
||
{
|
||
foreach ([self::profile(tested: ['1.2.0']), self::profile(Tier::ReportOnly)] as $profile) {
|
||
$html = self::html($this->tab($profile, self::report()));
|
||
|
||
self::assertStringNotContainsString('lit_mcp_compat_release', $html);
|
||
self::assertStringNotContainsString('Freigeben geht erst', $html);
|
||
}
|
||
}
|
||
|
||
public function testAnExistingReleaseCanBeWithdrawnWhateverTheState(): void
|
||
{
|
||
foreach ([ReleaseState::Released, ReleaseState::Incompatible] as $state) {
|
||
$profile = self::profile(Tier::ReleasePerVersion, self::matrix($state, $state === ReleaseState::Released ? [] : ['x']));
|
||
|
||
$html = self::html($this->tab($profile, self::report(), self::store(self::entry())));
|
||
|
||
self::assertStringContainsString('value="lit_mcp_compat_withdraw"', $html);
|
||
self::assertStringContainsString('value="nonce-lit_mcp_compat_withdraw_fake"', $html);
|
||
self::assertStringNotContainsString('value="lit_mcp_compat_release"', $html);
|
||
}
|
||
}
|
||
}
|