feat: Protokoll tab as a core-style list table
The change summary is the wide primary column (key "title", so it cannot
be hidden) with the tool's human label below and row actions "Revision
ansehen" / "Objekt öffnen". Objects get human names ("Seite: Kontakt",
"Menü: …", "Einstellungen: Titel der Website"; deleted/trashed marked),
primed per page with one query each for posts, menus and users. Times are
relative within 24 h. Non-ok results show a badge and a marked row instead
of an Ergebnis column. Page size and columns via Screen Options; the tool
filter shows labels grouped by integration, dates have visible labels,
and an active filter offers "Filter zurücksetzen" (also in the empty state).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -6,27 +6,31 @@ namespace LIT\MCP\Tests\Unit\Admin;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Admin\LogListTable;
|
||||
use LIT\MCP\Admin\LogObjects;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
final class LogListTableTest extends TestCase
|
||||
{
|
||||
private const NOW = 1790441353; // 2026-09-26 16:49:13 UTC
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\stubEscapeFunctions();
|
||||
Functions\stubTranslationFunctions();
|
||||
// Harmless defaults so single_row()'s full column pass doesn't blow up in tests
|
||||
// that only care about a subset of columns; overridden per test where needed.
|
||||
// Harmless defaults so a full row renders in tests that only care about one column.
|
||||
Functions\when('get_userdata')->justReturn(false);
|
||||
Functions\when('post_type_exists')->justReturn(false);
|
||||
Functions\when('get_post')->justReturn(null);
|
||||
Functions\when('get_term')->justReturn(null);
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
Functions\when('get_user_option')->justReturn(false);
|
||||
Functions\when('get_option')->alias(static fn (string $key) => $key === 'date_format' ? 'j. F Y' : 'H:i');
|
||||
Functions\when('wp_date')->alias(static fn (string $format, int $ts): string => $format . '@' . $ts);
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://kunde.de/wp-admin/' . $path);
|
||||
Functions\when('add_query_arg')->alias(static fn (array $args, string $url): string => $url . '?' . http_build_query($args));
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
@@ -35,232 +39,251 @@ final class LogListTableTest extends TestCase
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testPrepareItemsQueriesTheCurrentPageAndSetsPagination(): void
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
* @param array<string, string> $toolLabels
|
||||
*/
|
||||
private function table(array $filters = [], array $toolLabels = [], ?LogObjects $objects = null, ?ChangeLog $log = null): LogListTable
|
||||
{
|
||||
return new LogListTable(
|
||||
$log ?? Mockery::mock(ChangeLog::class),
|
||||
$filters + ['ability' => '', 'user_id' => 0],
|
||||
null,
|
||||
$toolLabels,
|
||||
$objects,
|
||||
self::NOW
|
||||
);
|
||||
}
|
||||
|
||||
public function testPrepareItemsUsesThePerPageScreenOptionAndPrimesUsersAndObjectsOnce(): void
|
||||
{
|
||||
$_GET['paged'] = '3';
|
||||
Functions\when('get_user_option')->alias(static fn (string $key) => $key === 'lit_mcp_log_per_page' ? '30' : false);
|
||||
$rows = [
|
||||
['id' => '2', 'user_id' => '3', 'object_type' => 'page', 'object_id' => '12'],
|
||||
['id' => '1', 'user_id' => '3', 'object_type' => 'page', 'object_id' => '13'],
|
||||
['id' => '0', 'user_id' => '4', 'object_type' => 'option', 'object_id' => 'blogname'],
|
||||
];
|
||||
$log = Mockery::mock(ChangeLog::class);
|
||||
$log->expects('query')
|
||||
->with(['ability' => 'lit/content-update', 'user_id' => 0], 50, 100)
|
||||
->andReturn(['rows' => [['id' => '1']], 'total' => 120]);
|
||||
->with(['ability' => 'lit/content-update', 'user_id' => 0], 30, 60)
|
||||
->andReturn(['rows' => $rows, 'total' => 120]);
|
||||
Functions\when('post_type_exists')->justReturn(true);
|
||||
Functions\expect('cache_users')->once()->with([3, 4]);
|
||||
Functions\expect('_prime_post_caches')->once()->with([12, 13], false, false);
|
||||
|
||||
$table = new LogListTable($log, ['ability' => 'lit/content-update', 'user_id' => 0]);
|
||||
$table = $this->table(['ability' => 'lit/content-update'], [], new LogObjects(), $log);
|
||||
$table->prepare_items();
|
||||
|
||||
self::assertSame([['id' => '1']], $table->items);
|
||||
self::assertSame(['total_items' => 120, 'per_page' => 50], $table->pagination_args());
|
||||
self::assertSame($rows, $table->items);
|
||||
self::assertSame(['total_items' => 120, 'per_page' => 30], $table->pagination_args());
|
||||
}
|
||||
|
||||
public function testColumnDefaultEscapesUntrustedClientAndSummaryText(): void
|
||||
public function testPrepareItemsDefaultsToTwentyPerPage(): void
|
||||
{
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
$log = Mockery::mock(ChangeLog::class);
|
||||
$log->expects('query')->with(['ability' => '', 'user_id' => 0], 20, 0)->andReturn(['rows' => [], 'total' => 0]);
|
||||
|
||||
$item = ['client' => 'Claude <script>alert(1)</script>', 'summary' => '<b>böse</b> Zusammenfassung'];
|
||||
$table = $this->table([], [], null, $log);
|
||||
$table->prepare_items();
|
||||
|
||||
self::assertStringNotContainsString('<script>', $this->columnDefault($table, $item, 'client'));
|
||||
self::assertStringContainsString('<script>', $this->columnDefault($table, $item, 'client'));
|
||||
self::assertStringNotContainsString('<b>', $this->columnDefault($table, $item, 'summary'));
|
||||
self::assertSame(['total_items' => 0, 'per_page' => 20], $table->pagination_args());
|
||||
}
|
||||
|
||||
public function testColumnDefaultShowsUserDisplayNameOrFallback(): void
|
||||
public function testColumnsPutTheChangeFirstAsTheUnhideablePrimaryColumn(): void
|
||||
{
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Jan <b>']);
|
||||
$table = $this->table();
|
||||
|
||||
$withUser = $this->columnDefault($table, ['user_id' => 3], 'user');
|
||||
self::assertStringContainsString('Jan <b>', $withUser);
|
||||
self::assertSame(
|
||||
['title' => 'Änderung', 'object' => 'Objekt', 'user' => 'Wer', 'created_at' => 'Zeit'],
|
||||
$table->get_columns()
|
||||
);
|
||||
// "title" like core's own primary columns: WordPress never offers it for hiding (WP_Screen).
|
||||
self::assertSame('title', $this->invoke($table, 'get_default_primary_column_name'));
|
||||
}
|
||||
|
||||
public function testColumnDefaultUserFallsBackToIdWhenUserIsGone(): void
|
||||
public function testChangeColumnShowsTheSummaryAndTheToolsHumanLabel(): void
|
||||
{
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
Functions\when('get_userdata')->justReturn(false);
|
||||
$table = $this->table([], ['lit/media-upload-from-url' => 'Datei von URL <importieren>']);
|
||||
|
||||
self::assertSame('#7', $this->columnDefault($table, ['user_id' => 7], 'user'));
|
||||
$html = $this->column($table, ['summary' => 'Datei <b>X</b> importiert', 'ability' => 'lit/media-upload-from-url', 'result' => 'ok'], 'title');
|
||||
|
||||
self::assertStringContainsString('Datei <b>X</b> importiert', $html);
|
||||
self::assertStringContainsString(
|
||||
'<span class="lit-mcp-muted" title="lit/media-upload-from-url">Datei von URL <importieren></span>',
|
||||
$html
|
||||
);
|
||||
self::assertStringNotContainsString('lit-mcp-badge', $html);
|
||||
}
|
||||
|
||||
public function testColumnDefaultLinksTheObjectForAKnownPostType(): void
|
||||
public function testChangeColumnFallsBackToTheToolNameForAnUnknownTool(): void
|
||||
{
|
||||
Functions\when('post_type_exists')->justReturn(true);
|
||||
Functions\when('get_edit_post_link')->justReturn('https://kunde.de/wp-admin/post.php?post=12&action=edit');
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
$html = $this->column($this->table(), ['summary' => 'x', 'ability' => 'lit/gone<', 'result' => 'ok'], 'title');
|
||||
|
||||
$html = $this->columnDefault($table, ['object_type' => 'page', 'object_id' => '12'], 'object');
|
||||
|
||||
self::assertStringContainsString('href="https://kunde.de/wp-admin/post.php?post=12&action=edit"', $html);
|
||||
self::assertStringContainsString('page 12', $html);
|
||||
self::assertStringContainsString('<span class="lit-mcp-muted">lit/gone<</span>', $html);
|
||||
}
|
||||
|
||||
public function testColumnDefaultShowsPlainLabelWhenGetEditPostLinkRefusesIt(): void
|
||||
/** @dataProvider notOk */
|
||||
public function testANotOkResultGetsABadgeAndAMarkedRow(string $result, string $class, string $word): void
|
||||
{
|
||||
Functions\when('post_type_exists')->justReturn(true);
|
||||
Functions\when('get_edit_post_link')->justReturn(null);
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
$table = $this->table();
|
||||
|
||||
$html = $this->columnDefault($table, ['object_type' => 'page', 'object_id' => '12'], 'object');
|
||||
$html = $this->column($table, ['summary' => 'x', 'ability' => 'lit/a', 'result' => $result], 'title');
|
||||
self::assertStringContainsString('<span class="lit-mcp-badge lit-mcp-badge-' . $class . '">' . $word . '</span>', $html);
|
||||
|
||||
self::assertSame('page 12', $html);
|
||||
}
|
||||
|
||||
public function testColumnDefaultLinksAnAttachmentToTheMediaEditScreen(): void
|
||||
{
|
||||
Functions\when('post_type_exists')->justReturn(true);
|
||||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 5]));
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://kunde.de/wp-admin/' . $path);
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
|
||||
$html = $this->columnDefault($table, ['object_type' => 'attachment', 'object_id' => '5'], 'object');
|
||||
|
||||
self::assertStringContainsString('href="https://kunde.de/wp-admin/upload.php?item=5"', $html);
|
||||
}
|
||||
|
||||
public function testColumnDefaultShowsPlainLabelForADeletedAttachment(): void
|
||||
{
|
||||
Functions\when('get_post')->justReturn(null);
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
|
||||
self::assertSame('attachment 5', $this->columnDefault($table, ['object_type' => 'attachment', 'object_id' => '5'], 'object'));
|
||||
}
|
||||
|
||||
public function testColumnDefaultLinksANavMenuToTheMenusScreen(): void
|
||||
{
|
||||
Functions\when('get_term')->justReturn(new WP_Term(['term_id' => 3, 'taxonomy' => 'nav_menu']));
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://kunde.de/wp-admin/' . $path);
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
|
||||
$html = $this->columnDefault($table, ['object_type' => 'nav_menu', 'object_id' => '3'], 'object');
|
||||
|
||||
self::assertStringContainsString('href="https://kunde.de/wp-admin/nav-menus.php?menu=3"', $html);
|
||||
}
|
||||
|
||||
/** @dataProvider unlinkableObjectTypes */
|
||||
public function testColumnDefaultNeverLinksSettingsOrMenuLocations(string $type, string $id): void
|
||||
{
|
||||
Functions\when('post_type_exists')->justReturn(false);
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
|
||||
self::assertSame(trim($type . ' ' . $id), $this->columnDefault($table, ['object_type' => $type, 'object_id' => $id], 'object'));
|
||||
ob_start();
|
||||
$table->single_row(['result' => $result, 'summary' => 'x', 'ability' => 'lit/a']);
|
||||
self::assertStringStartsWith('<tr class="lit-mcp-row-' . $class . '">', (string) ob_get_clean());
|
||||
}
|
||||
|
||||
/** @return list<list<string>> */
|
||||
public static function unlinkableObjectTypes(): array
|
||||
public static function notOk(): array
|
||||
{
|
||||
return [
|
||||
['option', 'blogname,blogdescription'],
|
||||
['menu_location', 'primary'],
|
||||
['unknown', '42'],
|
||||
['partial', 'warning', 'teilweise'],
|
||||
['failed', 'error', 'fehlgeschlagen'],
|
||||
['<b>kaputt', 'error', 'unbekannt'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testRevisionColumnShowsADashWithoutARevision(): void
|
||||
public function testAnOkRowIsNotMarked(): void
|
||||
{
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
ob_start();
|
||||
$this->table()->single_row(['result' => 'ok', 'summary' => 'x', 'ability' => 'lit/a']);
|
||||
|
||||
self::assertSame('–', $this->columnDefault($table, ['revision_id' => 0], 'revision'));
|
||||
self::assertStringStartsWith('<tr>', (string) ob_get_clean());
|
||||
}
|
||||
|
||||
public function testRevisionColumnLinksOnlyWhenTheUserCanEditTheParentPost(): void
|
||||
public function testRowActionsOfferTheRevisionAndTheObjectOnlyWhereTheyExist(): void
|
||||
{
|
||||
$objects = Mockery::mock(LogObjects::class);
|
||||
$objects->allows('describe')->with('page', '12')->andReturn(['html' => 'Seite: X', 'url' => 'https://kunde.de/wp-admin/post.php?post=12&action=edit']);
|
||||
$objects->allows('describe')->with('option', 'blogname')->andReturn(['html' => 'Einstellung: Titel', 'url' => '']);
|
||||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 811, 'post_type' => 'revision', 'post_parent' => 12]));
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://kunde.de/wp-admin/' . $path);
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
Functions\when('current_user_can')->alias(static fn (string $cap, int $id = 0): bool => $cap === 'edit_post' && $id === 12);
|
||||
$table = $this->table([], [], $objects);
|
||||
|
||||
$html = $this->columnDefault($table, ['revision_id' => 811], 'revision');
|
||||
$both = $this->rowActions($table, ['object_type' => 'page', 'object_id' => '12', 'revision_id' => '811']);
|
||||
self::assertStringContainsString('<a href="https://kunde.de/wp-admin/revision.php?revision=811">Revision ansehen</a>', $both);
|
||||
self::assertStringContainsString('<a href="https://kunde.de/wp-admin/post.php?post=12&action=edit">Objekt öffnen</a>', $both);
|
||||
|
||||
self::assertStringContainsString('href="https://kunde.de/wp-admin/revision.php?revision=811"', $html);
|
||||
$none = $this->rowActions($table, ['object_type' => 'option', 'object_id' => 'blogname', 'revision_id' => null]);
|
||||
self::assertStringNotContainsString('row-actions', $none);
|
||||
self::assertStringContainsString('toggle-row', $none, 'the mobile toggle stays');
|
||||
self::assertSame('', $this->invoke($table, 'handle_row_actions', ['object_type' => 'option'], 'user', 'title'));
|
||||
}
|
||||
|
||||
public function testRevisionColumnHidesTheLinkWithoutEditPermission(): void
|
||||
public function testRevisionActionNeedsEditRightsOnTheParentPost(): void
|
||||
{
|
||||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 811, 'post_type' => 'revision', 'post_parent' => 12]));
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
|
||||
self::assertSame('–', $this->columnDefault($table, ['revision_id' => 811], 'revision'));
|
||||
$html = $this->rowActions($this->table(), ['object_type' => 'x', 'object_id' => '1', 'revision_id' => '811']);
|
||||
|
||||
self::assertStringNotContainsString('revision.php', $html);
|
||||
}
|
||||
|
||||
public function testSingleRowHighlightsPartialResults(): void
|
||||
public function testObjectColumnUsesTheHumanObjectDescription(): void
|
||||
{
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
$objects = Mockery::mock(LogObjects::class);
|
||||
$objects->expects('describe')->with('page', '12')->andReturn(['html' => 'Seite: <a href="u">X</a>', 'url' => 'u']);
|
||||
|
||||
self::assertSame('Seite: <a href="u">X</a>', $this->column($this->table([], [], $objects), ['object_type' => 'page', 'object_id' => '12'], 'object'));
|
||||
}
|
||||
|
||||
public function testUserColumnShowsTheNameAndTheShortenedClientWithFullNameAsTitle(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Jan <b>']);
|
||||
$client = 'lit-mcp-integration-test-client-<script>';
|
||||
|
||||
$html = $this->column($this->table(), ['user_id' => 3, 'client' => $client], 'user');
|
||||
|
||||
self::assertStringStartsWith('Jan <b><br>', $html);
|
||||
self::assertStringContainsString('title="lit-mcp-integration-test-client-<script>"', $html);
|
||||
self::assertStringContainsString('>lit-mcp-integration-t…</span>', $html);
|
||||
self::assertStringNotContainsString('<script>', $html);
|
||||
}
|
||||
|
||||
public function testUserColumnFallsBackToTheIdAndOmitsAnEmptyClient(): void
|
||||
{
|
||||
self::assertSame('#7', $this->column($this->table(), ['user_id' => 7, 'client' => ''], 'user'));
|
||||
self::assertSame(
|
||||
'#7<br><span class="lit-mcp-muted" title="Claude">Claude</span>',
|
||||
$this->column($this->table(), ['user_id' => 7, 'client' => 'Claude'], 'user')
|
||||
);
|
||||
}
|
||||
|
||||
public function testTimeIsRelativeWithinADay(): void
|
||||
{
|
||||
Functions\when('human_time_diff')->alias(static fn (int $from, int $to): string => (($to - $from) / 60) . ' Minuten');
|
||||
|
||||
$html = $this->column($this->table(), ['created_at' => '2026-09-26 16:44:13'], 'created_at');
|
||||
|
||||
self::assertStringContainsString('>vor 5 Minuten</time>', $html);
|
||||
self::assertStringContainsString('datetime="2026-09-26T16:44:13+00:00"', $html);
|
||||
self::assertStringContainsString('title="j. F Y@' . (self::NOW - 300) . ' H:i@' . (self::NOW - 300) . ' (UTC: 2026-09-26 16:44:13)"', $html);
|
||||
}
|
||||
|
||||
public function testOlderTimesShowTheSiteDateAndTime(): void
|
||||
{
|
||||
$ts = gmmktime(10, 39, 13, 9, 24, 2026);
|
||||
|
||||
$html = $this->column($this->table(), ['created_at' => '2026-09-24 10:39:13'], 'created_at');
|
||||
|
||||
self::assertStringContainsString('>j. F Y@' . $ts . ' um H:i@' . $ts . '</time>', $html);
|
||||
self::assertSame('–', $this->column($this->table(), ['created_at' => 'kaputt'], 'created_at'));
|
||||
}
|
||||
|
||||
public function testEmptyStateWithoutFilters(): void
|
||||
{
|
||||
ob_start();
|
||||
$table->single_row(['result' => 'partial']);
|
||||
$partial = (string) ob_get_clean();
|
||||
$this->table()->no_items();
|
||||
|
||||
self::assertSame('Noch keine Änderungen protokolliert.', (string) ob_get_clean());
|
||||
}
|
||||
|
||||
public function testEmptyStateWithFiltersOffersTheReset(): void
|
||||
{
|
||||
ob_start();
|
||||
$table->single_row(['result' => 'ok']);
|
||||
$ok = (string) ob_get_clean();
|
||||
$this->table(['result' => 'partial'])->no_items();
|
||||
|
||||
self::assertStringContainsString('<tr class="lit-mcp-partial">', $partial);
|
||||
self::assertSame('<tr>', substr($ok, 0, 4));
|
||||
self::assertSame(
|
||||
'Keine Einträge für diese Filter. <a href="https://kunde.de/wp-admin/options-general.php?page=lit-mcp&tab=protokoll">Filter zurücksetzen</a>',
|
||||
(string) ob_get_clean()
|
||||
);
|
||||
}
|
||||
|
||||
public function testTimeColumnShowsSiteTimeInWordPressFormatWithUtcAsTitle(): void
|
||||
{
|
||||
Functions\when('get_option')->alias(static fn (string $key) => $key === 'date_format' ? 'd.m.Y' : 'H:i');
|
||||
Functions\when('wp_date')->alias(static fn (string $format, int $ts): string => $format . '@' . $ts);
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
|
||||
self::assertSame('Zeit', $table->get_columns()['created_at']);
|
||||
$html = $this->column($table, ['created_at' => '2026-09-26 10:39:13'], 'created_at');
|
||||
|
||||
self::assertStringContainsString('d.m.Y H:i@' . gmmktime(10, 39, 13, 9, 26, 2026), $html);
|
||||
self::assertStringContainsString('title="UTC: 2026-09-26 10:39:13"', $html);
|
||||
self::assertSame('–', $this->column($table, ['created_at' => 'kaputt'], 'created_at'));
|
||||
}
|
||||
|
||||
/** @dataProvider results */
|
||||
public function testResultColumnShowsIconAndWord(string $result, string $icon, string $word): void
|
||||
{
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
|
||||
|
||||
$html = $this->column($table, ['result' => $result], 'result');
|
||||
|
||||
self::assertStringContainsString('dashicons-' . $icon, $html);
|
||||
self::assertStringContainsString($word, $html);
|
||||
self::assertStringNotContainsString('error', $html);
|
||||
}
|
||||
|
||||
/** @return list<list<string>> */
|
||||
public static function results(): array
|
||||
{
|
||||
return [
|
||||
['ok', 'yes-alt', 'erfolgreich'],
|
||||
['partial', 'warning', 'teilweise'],
|
||||
['error<b>', 'dismiss', 'unbekannt'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testToolColumnIsMutedTextAndTopNavRendersTheGivenFilters(): void
|
||||
public function testTopNavRendersTheGivenFilters(): void
|
||||
{
|
||||
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0], static function (): void {
|
||||
echo '<select name="ability"></select>';
|
||||
});
|
||||
|
||||
self::assertSame(
|
||||
'<span class="lit-mcp-tool-name">lit/x<</span>',
|
||||
$this->column($table, ['ability' => 'lit/x<'], 'ability')
|
||||
);
|
||||
|
||||
ob_start();
|
||||
$table->display();
|
||||
|
||||
self::assertStringContainsString('<div class="alignleft actions"><select name="ability"></select></div>', (string) ob_get_clean());
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $item */
|
||||
private function rowActions(LogListTable $table, array $item): string
|
||||
{
|
||||
return (string) $this->invoke($table, 'handle_row_actions', $item, 'title', 'title');
|
||||
}
|
||||
|
||||
private function invoke(LogListTable $table, string $method, mixed ...$args): mixed
|
||||
{
|
||||
return (new \ReflectionMethod($table, $method))->invoke($table, ...$args);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $item */
|
||||
private function column(LogListTable $table, array $item, string $column): string
|
||||
{
|
||||
$method = 'column_' . $column;
|
||||
if (method_exists($table, $method)) {
|
||||
return (string) (new \ReflectionMethod($table, $method))->invoke($table, $item);
|
||||
return (string) $this->invoke($table, $method, $item);
|
||||
}
|
||||
|
||||
return $this->columnDefault($table, $item, $column);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $item */
|
||||
private function columnDefault(LogListTable $table, array $item, string $column): string
|
||||
{
|
||||
return (string) (new \ReflectionMethod($table, 'column_default'))->invoke($table, $item, $column);
|
||||
return (string) $this->invoke($table, 'column_default', $item, $column);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Admin;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Admin\LogObjects;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
final class LogObjectsTest extends TestCase
|
||||
{
|
||||
/** @var array<int, WP_Post> */
|
||||
private array $posts = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\stubEscapeFunctions();
|
||||
Functions\stubTranslationFunctions();
|
||||
Functions\when('post_type_exists')->alias(static fn (string $type): bool => in_array($type, ['page', 'post', 'attachment'], true));
|
||||
Functions\when('get_post_type_object')->alias(static fn (string $type): object => (object) [
|
||||
'labels' => (object) ['singular_name' => ['page' => 'Seite', 'post' => 'Beitrag'][$type] ?? $type],
|
||||
]);
|
||||
Functions\when('get_post')->alias(fn ($id) => $this->posts[(int) $id] ?? null);
|
||||
Functions\when('get_edit_post_link')->alias(
|
||||
static fn ($id, $context = 'display'): string => 'https://kunde.de/wp-admin/post.php?post=' . $id . '&action=edit'
|
||||
);
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://kunde.de/wp-admin/' . $path);
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('get_registered_nav_menus')->justReturn(['primary-menu' => 'Hauptmenü']);
|
||||
}
|
||||
|
||||
public function testAPageShowsItsTypeLabelAndLinkedTitle(): void
|
||||
{
|
||||
$this->posts[834] = new WP_Post(['ID' => 834, 'post_type' => 'page', 'post_status' => 'publish', 'post_title' => '[Test] <b>Kontakt</b>']);
|
||||
|
||||
$object = (new LogObjects())->describe('page', '834');
|
||||
|
||||
self::assertSame(
|
||||
'Seite: <a href="https://kunde.de/wp-admin/post.php?post=834&action=edit">[Test] <b>Kontakt</b></a>',
|
||||
$object['html']
|
||||
);
|
||||
self::assertSame('https://kunde.de/wp-admin/post.php?post=834&action=edit', $object['url']);
|
||||
}
|
||||
|
||||
public function testAnUntitledPostSaysSo(): void
|
||||
{
|
||||
$this->posts[5] = new WP_Post(['ID' => 5, 'post_type' => 'post', 'post_status' => 'draft', 'post_title' => '']);
|
||||
|
||||
self::assertStringContainsString('>(kein Titel)</a>', (new LogObjects())->describe('post', '5')['html']);
|
||||
}
|
||||
|
||||
public function testNoLinkWhenTheUserMayNotEditThePost(): void
|
||||
{
|
||||
Functions\when('get_edit_post_link')->justReturn(null);
|
||||
$this->posts[834] = new WP_Post(['ID' => 834, 'post_type' => 'page', 'post_status' => 'publish', 'post_title' => 'Kontakt']);
|
||||
|
||||
self::assertSame(['html' => 'Seite: Kontakt', 'url' => ''], (new LogObjects())->describe('page', '834'));
|
||||
}
|
||||
|
||||
public function testATrashedPostIsNamedButNotLinked(): void
|
||||
{
|
||||
$this->posts[783] = new WP_Post(['ID' => 783, 'post_type' => 'page', 'post_status' => 'trash', 'post_title' => 'LIT Smoke Seite']);
|
||||
|
||||
self::assertSame(
|
||||
['html' => 'Seite: LIT Smoke Seite (im Papierkorb)', 'url' => ''],
|
||||
(new LogObjects())->describe('page', '783')
|
||||
);
|
||||
}
|
||||
|
||||
public function testADeletedPostShowsItsIdAsDeleted(): void
|
||||
{
|
||||
self::assertSame(['html' => 'Seite: #834 (gelöscht)', 'url' => ''], (new LogObjects())->describe('page', '834'));
|
||||
}
|
||||
|
||||
public function testAnAttachmentIsAMediumLinkedToTheMediaEditScreen(): void
|
||||
{
|
||||
$this->posts[830] = new WP_Post(['ID' => 830, 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_title' => 'Massivholzmoebel']);
|
||||
|
||||
$object = (new LogObjects())->describe('attachment', '830');
|
||||
|
||||
self::assertSame(
|
||||
'Medium: <a href="https://kunde.de/wp-admin/post.php?post=830&action=edit">Massivholzmoebel</a>',
|
||||
$object['html']
|
||||
);
|
||||
self::assertSame('Medium: #799 (gelöscht)', (new LogObjects())->describe('attachment', '799')['html']);
|
||||
}
|
||||
|
||||
public function testANavMenuIsLinkedToTheMenusScreen(): void
|
||||
{
|
||||
Functions\when('get_terms')->justReturn([new WP_Term(['term_id' => 21, 'name' => 'Haupt<menü>', 'taxonomy' => 'nav_menu'])]);
|
||||
$objects = new LogObjects();
|
||||
$objects->prime([['object_type' => 'nav_menu', 'object_id' => '21'], ['object_type' => 'nav_menu', 'object_id' => '22']]);
|
||||
|
||||
self::assertSame(
|
||||
[
|
||||
'html' => 'Menü: <a href="https://kunde.de/wp-admin/nav-menus.php?action=edit&menu=21">Haupt<menü></a>',
|
||||
'url' => 'https://kunde.de/wp-admin/nav-menus.php?action=edit&menu=21',
|
||||
],
|
||||
$objects->describe('nav_menu', '21')
|
||||
);
|
||||
self::assertSame(['html' => 'Menü: #22 (gelöscht)', 'url' => ''], $objects->describe('nav_menu', '22'));
|
||||
}
|
||||
|
||||
public function testANavMenuIsNotLinkedWithoutTheMenuCapability(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
Functions\when('get_terms')->justReturn([new WP_Term(['term_id' => 21, 'name' => 'Hauptmenü', 'taxonomy' => 'nav_menu'])]);
|
||||
$objects = new LogObjects();
|
||||
$objects->prime([['object_type' => 'nav_menu', 'object_id' => '21']]);
|
||||
|
||||
self::assertSame(['html' => 'Menü: Hauptmenü', 'url' => ''], $objects->describe('nav_menu', '21'));
|
||||
}
|
||||
|
||||
public function testAMenuLocationShowsItsRegisteredLabel(): void
|
||||
{
|
||||
self::assertSame(['html' => 'Menüposition: Hauptmenü', 'url' => ''], (new LogObjects())->describe('menu_location', 'primary-menu'));
|
||||
self::assertSame(
|
||||
['html' => 'Menüposition: <code>footer<x></code>', 'url' => ''],
|
||||
(new LogObjects())->describe('menu_location', 'footer<x>')
|
||||
);
|
||||
}
|
||||
|
||||
public function testOptionsShowHumanLabelsAndUnknownKeysAsCode(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['html' => 'Einstellungen: Titel der Website, Untertitel', 'url' => ''],
|
||||
(new LogObjects())->describe('option', 'blogname,blogdescription')
|
||||
);
|
||||
self::assertSame(
|
||||
['html' => 'Einstellung: <code>foo<b></code>', 'url' => ''],
|
||||
(new LogObjects())->describe('option', 'foo<b>')
|
||||
);
|
||||
}
|
||||
|
||||
public function testAnUnknownObjectTypeIsShownAsCode(): void
|
||||
{
|
||||
self::assertSame(['html' => '<code>shop_thing 42</code>', 'url' => ''], (new LogObjects())->describe('shop_thing', '42'));
|
||||
self::assertSame(['html' => '–', 'url' => ''], (new LogObjects())->describe('', ''));
|
||||
}
|
||||
|
||||
public function testPrimeLoadsAllPostsAndMenusOfThePageInOneGoEach(): void
|
||||
{
|
||||
Functions\expect('_prime_post_caches')->once()->with([834, 830], false, false);
|
||||
Functions\expect('get_terms')->once()->with(['taxonomy' => 'nav_menu', 'include' => [21], 'hide_empty' => false])->andReturn([]);
|
||||
|
||||
(new LogObjects())->prime([
|
||||
['object_type' => 'page', 'object_id' => '834'],
|
||||
['object_type' => 'attachment', 'object_id' => '830'],
|
||||
['object_type' => 'page', 'object_id' => '834'],
|
||||
['object_type' => 'nav_menu', 'object_id' => '21'],
|
||||
['object_type' => 'option', 'object_id' => 'blogname'],
|
||||
['object_type' => 'page', 'object_id' => 'x'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testPrimeQueriesNothingWithoutPostsOrMenus(): void
|
||||
{
|
||||
Functions\expect('_prime_post_caches')->never();
|
||||
Functions\expect('get_terms')->never();
|
||||
|
||||
(new LogObjects())->prime([['object_type' => 'option', 'object_id' => 'blogname']]);
|
||||
}
|
||||
}
|
||||
+127
-36
@@ -5,8 +5,11 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Admin;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Admin\LogTab;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
use LIT\MCP\Limits;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
@@ -16,13 +19,45 @@ final class LogTabTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($_GET['ability'], $_GET['user_id'], $_GET['result'], $_GET['date_from'], $_GET['date_to']);
|
||||
unset($_GET['ability'], $_GET['user_id'], $_GET['result'], $_GET['date_from'], $_GET['date_to'], $_GET['tab']);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function tab(ChangeLog $log): LogTab
|
||||
{
|
||||
return new LogTab($log, new Limits(), $this->registry());
|
||||
}
|
||||
|
||||
private function registry(): IntegrationRegistry
|
||||
{
|
||||
return new IntegrationRegistry(
|
||||
$this->integration('media', 'Medien', ['lit/media-upload-from-url' => 'Datei von URL importieren']),
|
||||
$this->integration('wordpress', 'WordPress', ['lit/content-update' => 'Inhalt <ändern>'])
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, string> $abilities name => label */
|
||||
private function integration(string $id, string $label, array $abilities): Integration
|
||||
{
|
||||
$mocks = [];
|
||||
foreach ($abilities as $name => $abilityLabel) {
|
||||
$ability = Mockery::mock(Ability::class);
|
||||
$ability->allows('name')->andReturn($name);
|
||||
$ability->allows('label')->andReturn($abilityLabel);
|
||||
$mocks[] = $ability;
|
||||
}
|
||||
$integration = Mockery::mock(Integration::class);
|
||||
$integration->allows('id')->andReturn($id);
|
||||
$integration->allows('label')->andReturn($label);
|
||||
$integration->allows('isAvailable')->andReturn(true);
|
||||
$integration->allows('abilities')->andReturn($mocks);
|
||||
|
||||
return $integration;
|
||||
}
|
||||
|
||||
public function testFiltersAreReadSanitized(): void
|
||||
{
|
||||
$tab = new LogTab(Mockery::mock(ChangeLog::class), new Limits());
|
||||
$tab = $this->tab(Mockery::mock(ChangeLog::class));
|
||||
$_GET['ability'] = 'lit/content-update';
|
||||
$_GET['user_id'] = '3x';
|
||||
|
||||
@@ -34,7 +69,7 @@ final class LogTabTest extends TestCase
|
||||
|
||||
public function testExtraFiltersWhitelistResultAndDateRange(): void
|
||||
{
|
||||
$tab = new LogTab(Mockery::mock(ChangeLog::class), new Limits());
|
||||
$tab = $this->tab(Mockery::mock(ChangeLog::class));
|
||||
|
||||
self::assertSame(
|
||||
['result' => '', 'date_from' => '', 'date_to' => ''],
|
||||
@@ -54,7 +89,7 @@ final class LogTabTest extends TestCase
|
||||
|
||||
public function testExtraFiltersRejectUnknownResultAndMalformedDates(): void
|
||||
{
|
||||
$tab = new LogTab(Mockery::mock(ChangeLog::class), new Limits());
|
||||
$tab = $this->tab(Mockery::mock(ChangeLog::class));
|
||||
|
||||
$_GET['result'] = 'ok"><script>';
|
||||
$_GET['date_from'] = 'not-a-date';
|
||||
@@ -70,7 +105,7 @@ final class LogTabTest extends TestCase
|
||||
public function testFiltersRejectATrailingNewlineEvenWithoutTheSanitizer(): void
|
||||
{
|
||||
Functions\when('sanitize_text_field')->returnArg();
|
||||
$tab = new LogTab(Mockery::mock(ChangeLog::class), new Limits());
|
||||
$tab = $this->tab(Mockery::mock(ChangeLog::class));
|
||||
$_GET['ability'] = "lit/content-update\n";
|
||||
$_GET['date_from'] = "2026-09-01\n";
|
||||
$_GET['date_to'] = "2026-09-25\n";
|
||||
@@ -93,7 +128,7 @@ final class LogTabTest extends TestCase
|
||||
$log->expects('clear')->once();
|
||||
$terminated = false;
|
||||
|
||||
(new LogTab($log, new Limits(), static function () use (&$terminated): void {
|
||||
(new LogTab($log, new Limits(), $this->registry(), static function () use (&$terminated): void {
|
||||
$terminated = true;
|
||||
}))->handleClear();
|
||||
|
||||
@@ -110,24 +145,100 @@ final class LogTabTest extends TestCase
|
||||
$log->expects('clear')->never();
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
(new LogTab($log, new Limits()))->handleClear();
|
||||
$this->tab($log)->handleClear();
|
||||
}
|
||||
|
||||
public function testRegistersTheClearAction(): void
|
||||
public function testRegistersTheClearActionAndTheScreenOptions(): void
|
||||
{
|
||||
$tab = new LogTab(Mockery::mock(ChangeLog::class), new Limits());
|
||||
$tab = $this->tab(Mockery::mock(ChangeLog::class));
|
||||
Functions\expect('add_action')->once()->with('admin_post_lit_mcp_clear_log', [$tab, 'handleClear']);
|
||||
Functions\expect('add_action')->once()->with('load-settings_page_lit-mcp', [$tab, 'loadScreen']);
|
||||
Functions\expect('add_filter')->once()->with('set_screen_option_lit_mcp_log_per_page', [$tab, 'savePerPage'], 10, 3);
|
||||
|
||||
$tab->registerActions();
|
||||
}
|
||||
|
||||
public function testLoadScreenAddsThePerPageScreenOptionOnlyOnThisTab(): void
|
||||
{
|
||||
Functions\stubTranslationFunctions();
|
||||
Functions\when('sanitize_key')->returnArg();
|
||||
$tab = $this->tab(Mockery::mock(ChangeLog::class));
|
||||
|
||||
Functions\expect('add_screen_option')->never();
|
||||
$_GET['tab'] = 'tools';
|
||||
$tab->loadScreen();
|
||||
|
||||
Functions\expect('add_screen_option')->once()->with('per_page', [
|
||||
'label' => 'Einträge pro Seite',
|
||||
'default' => 20,
|
||||
'option' => 'lit_mcp_log_per_page',
|
||||
]);
|
||||
$_GET['tab'] = 'protokoll';
|
||||
$tab->loadScreen();
|
||||
}
|
||||
|
||||
public function testSavePerPageKeepsOnlyItsOwnOption(): void
|
||||
{
|
||||
$tab = $this->tab(Mockery::mock(ChangeLog::class));
|
||||
|
||||
self::assertSame(40, $tab->savePerPage(false, 'lit_mcp_log_per_page', '40'));
|
||||
self::assertFalse($tab->savePerPage(false, 'other_per_page', '40'));
|
||||
}
|
||||
|
||||
public function testRenderShowsFiltersAndAConfirmProtectedClearButton(): void
|
||||
{
|
||||
$html = $this->render(['lit/content-update', 'lit/media-upload-from-url', 'lit/x"><script>']);
|
||||
|
||||
self::assertStringContainsString('name="result"', $html);
|
||||
self::assertStringNotContainsString('<script>', $html);
|
||||
self::assertStringContainsString('lit_mcp_clear_log', $html);
|
||||
self::assertStringContainsString('confirm(', $html);
|
||||
self::assertStringContainsString('nonce-lit_mcp_clear_log', $html);
|
||||
|
||||
// One intro line; the retention days link to the Limits tab.
|
||||
self::assertMatchesRegularExpression('#<p class="description">[^<]*<a href="[^"]*tab=limits">90 Tagen</a>#', $html);
|
||||
// Selects keep screen-reader labels; the dates have short visible ones.
|
||||
foreach (['ability', 'user-id', 'result'] as $field) {
|
||||
self::assertStringContainsString('<label class="screen-reader-text" for="lit-mcp-filter-' . $field . '">', $html);
|
||||
}
|
||||
self::assertStringContainsString('<label for="lit-mcp-filter-date-from">von</label>', $html);
|
||||
self::assertStringContainsString('<label for="lit-mcp-filter-date-to">bis</label>', $html);
|
||||
self::assertStringContainsString('<option value="ok">erfolgreich</option>', $html);
|
||||
self::assertStringContainsString('<option value="partial">teilweise</option>', $html);
|
||||
self::assertStringNotContainsString('Filter zurücksetzen', $html, 'no reset without an active filter');
|
||||
}
|
||||
|
||||
public function testToolFilterShowsHumanLabelsGroupedByIntegration(): void
|
||||
{
|
||||
$_GET['ability'] = 'lit/content-update';
|
||||
|
||||
$html = $this->render(['lit/content-update', 'lit/gone', 'lit/media-upload-from-url']);
|
||||
|
||||
self::assertStringContainsString(
|
||||
'<optgroup label="Medien"><option value="lit/media-upload-from-url">Datei von URL importieren</option></optgroup>'
|
||||
. '<optgroup label="WordPress"><option value="lit/content-update" selected="selected">Inhalt <ändern></option></optgroup>'
|
||||
. '<optgroup label="Weitere"><option value="lit/gone">lit/gone</option></optgroup>',
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
public function testAnActiveFilterOffersTheReset(): void
|
||||
{
|
||||
$_GET['date_from'] = '2026-09-01';
|
||||
|
||||
$html = $this->render([]);
|
||||
|
||||
self::assertStringContainsString(
|
||||
'<a class="lit-mcp-reset" href="https://kunde.de/wp-admin/options-general.php?page=lit-mcp&tab=protokoll">Filter zurücksetzen</a>',
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
/** @param list<string> $abilities abilities found in the log */
|
||||
private function render(array $abilities): string
|
||||
{
|
||||
Functions\stubEscapeFunctions();
|
||||
Functions\stubTranslationFunctions();
|
||||
Functions\when('selected')->alias(static fn ($a, $b = true, $echo = true): string => $a === $b ? ' selected="selected"' : '');
|
||||
Functions\when('checked')->justReturn('');
|
||||
Functions\when('settings_fields')->justReturn(null);
|
||||
Functions\when('submit_button')->alias(static function (
|
||||
string $text = '',
|
||||
string $type = 'primary',
|
||||
@@ -150,36 +261,16 @@ final class LogTabTest extends TestCase
|
||||
});
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://kunde.de/wp-admin/' . $path);
|
||||
Functions\when('get_option')->justReturn([]);
|
||||
Functions\when('get_user_option')->justReturn(false);
|
||||
Functions\when('add_query_arg')->alias(static fn (array $args, string $url): string => $url . '?' . http_build_query($args));
|
||||
|
||||
$log = Mockery::mock(ChangeLog::class);
|
||||
$log->allows('abilities')->andReturn(['lit/content-update', 'lit/x"><script>']);
|
||||
$log->allows('abilities')->andReturn($abilities);
|
||||
$log->allows('query')->andReturn(['rows' => [], 'total' => 0]);
|
||||
|
||||
ob_start();
|
||||
(new LogTab($log, new Limits()))->render();
|
||||
$html = (string) ob_get_clean();
|
||||
$this->tab($log)->render();
|
||||
|
||||
self::assertStringContainsString('name="result"', $html);
|
||||
self::assertStringContainsString('name="date_from"', $html);
|
||||
self::assertStringContainsString('name="date_to"', $html);
|
||||
self::assertStringContainsString('lit/content-update', $html);
|
||||
self::assertStringNotContainsString('<script>', $html);
|
||||
self::assertStringContainsString('lit_mcp_clear_log', $html);
|
||||
self::assertStringContainsString('confirm(', $html);
|
||||
self::assertStringContainsString('nonce-lit_mcp_clear_log', $html);
|
||||
|
||||
// One intro line; the retention days link to the Limits tab.
|
||||
self::assertMatchesRegularExpression('#<p class="description">[^<]*<a href="[^"]*tab=limits">90 Tagen</a>#', $html);
|
||||
// Every filter control has a (screen-reader) label.
|
||||
foreach (['ability', 'user_id', 'result', 'date_from', 'date_to'] as $name) {
|
||||
self::assertMatchesRegularExpression(
|
||||
'#<label class="screen-reader-text" for="lit-mcp-filter-' . str_replace('_', '-', $name) . '">#',
|
||||
$html,
|
||||
'label for ' . $name
|
||||
);
|
||||
}
|
||||
self::assertStringContainsString('<option value="ok">erfolgreich</option>', $html);
|
||||
self::assertStringContainsString('<option value="partial">teilweise</option>', $html);
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user