feat: admin settings page UI/UX pass with WordPress-native patterns
- Header: h1 + logo in a wrapping flex row, hr.wp-header-end before the tabs, aria-current on the active tab; own CSS/JS enqueued only on this screen. - Verbindung: ordered "So verbindest du Claude" steps, copy button (core clipboard.js + wp.a11y.speak), Site-Health-style status list, German details. - Zugriff: roles as form-table fieldset, connections with site date format, empty-state row, confirm on revoke; ConnectionUsage records last use via WP_Application_Passwords::record_application_password_usage (once a day). - Tools: plain-language legend, counter as inline notice, groups as core postboxes (postbox.js toggles, per-user closed state, summary when closed), fixed column widths with new "Art" column, submit above and below. - Protokoll: site-timezone times with UTC tooltip, result icons, filters with labels in the list table nav, retention link to Limits. - Limits: short labels with units and descriptions; snapshot count hidden but preserved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -171,21 +171,36 @@ final class AccessTabTest extends TestCase
|
||||
(new AccessTab(new Access(), $connections))->handleRevoke();
|
||||
}
|
||||
|
||||
public function testRenderShowsRolesAndConnections(): void
|
||||
/** @param mixed $roles value of the roles option */
|
||||
private function stubRender(mixed $roles): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn(['editor']);
|
||||
Functions\when('get_option')->alias(static fn (string $key, $default = false) => match ($key) {
|
||||
'lit_mcp_allowed_roles' => $roles,
|
||||
'date_format' => 'd.m.Y',
|
||||
'time_format' => 'H:i',
|
||||
default => $default,
|
||||
});
|
||||
Functions\when('translate_user_role')->returnArg();
|
||||
Functions\when('checked')->alias(static fn ($a, $b = true, $echo = true): string => $a === $b ? ' checked="checked"' : '');
|
||||
Functions\when('settings_fields')->justReturn(null);
|
||||
Functions\when('submit_button')->alias(static function (): void {
|
||||
echo '<button>Speichern</button>';
|
||||
Functions\when('submit_button')->alias(static function ($text = null, $type = 'primary', $name = 'submit', $wrap = true, $other = ''): void {
|
||||
$attrs = is_array($other) ? implode(' ', array_map(
|
||||
static fn ($k, $v): string => sprintf('%s="%s"', $k, $v),
|
||||
array_keys($other),
|
||||
$other
|
||||
)) : '';
|
||||
echo '<button ' . $attrs . '>' . ($text ?? 'Speichern') . '</button>';
|
||||
});
|
||||
Functions\when('wp_nonce_field')->alias(static function (string $action): void {
|
||||
echo '<input type="hidden" name="_wpnonce" value="nonce-' . $action . '">';
|
||||
});
|
||||
Functions\when('wp_date')->justReturn('25.09.2026 10:00');
|
||||
$connections = Mockery::mock(ConnectionRepository::class);
|
||||
$connections->allows('all')->andReturn([[
|
||||
Functions\when('wp_date')->alias(static fn (string $format, int $ts): string => $format . '@' . $ts);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $overrides */
|
||||
private function connection(array $overrides = []): array
|
||||
{
|
||||
return $overrides + [
|
||||
'user_id' => 3,
|
||||
'user_login' => 'redaktion<b>',
|
||||
'uuid' => 'uuid-1',
|
||||
@@ -193,38 +208,73 @@ final class AccessTabTest extends TestCase
|
||||
'app_id' => 'https://claude.ai/x',
|
||||
'created' => 1790000000,
|
||||
'last_used' => null,
|
||||
]]);
|
||||
];
|
||||
}
|
||||
|
||||
private function renderTab(array $connections): string
|
||||
{
|
||||
$repository = Mockery::mock(ConnectionRepository::class);
|
||||
$repository->allows('all')->andReturn($connections);
|
||||
|
||||
ob_start();
|
||||
(new AccessTab(new Access(), $connections))->render();
|
||||
$html = (string) ob_get_clean();
|
||||
(new AccessTab(new Access(), $repository))->render();
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
public function testRenderShowsRolesAndConnections(): void
|
||||
{
|
||||
$this->stubRender(['editor']);
|
||||
|
||||
$html = $this->renderTab([$this->connection()]);
|
||||
|
||||
self::assertStringContainsString('value="editor" checked="checked"', $html);
|
||||
self::assertStringNotContainsString('value="administrator" checked', $html);
|
||||
self::assertStringContainsString('Claude <script>', $html);
|
||||
self::assertStringContainsString('<strong>Claude <script></strong>', $html);
|
||||
self::assertStringContainsString('redaktion<b>', $html);
|
||||
self::assertStringNotContainsString('<script>', $html);
|
||||
self::assertStringNotContainsString('<code>', $html);
|
||||
self::assertStringContainsString('value="uuid-1"', $html);
|
||||
self::assertStringContainsString('name="user_id" value="3"', $html);
|
||||
self::assertStringContainsString('value="lit_mcp_revoke"', $html);
|
||||
self::assertStringContainsString('nonce-lit_mcp_revoke_uuid-1', $html);
|
||||
self::assertStringContainsString('confirm(', $html);
|
||||
}
|
||||
|
||||
public function testRenderWithoutConnections(): void
|
||||
public function testRolesAreAFormTableFieldsetWithLegendAndDescription(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn(['administrator']);
|
||||
Functions\when('translate_user_role')->returnArg();
|
||||
Functions\when('checked')->justReturn('');
|
||||
Functions\when('settings_fields')->justReturn(null);
|
||||
Functions\when('submit_button')->justReturn(null);
|
||||
$connections = Mockery::mock(ConnectionRepository::class);
|
||||
$connections->allows('all')->andReturn([]);
|
||||
$this->stubRender(['editor']);
|
||||
|
||||
ob_start();
|
||||
(new AccessTab(new Access(), $connections))->render();
|
||||
$html = (string) ob_get_clean();
|
||||
$html = $this->renderTab([]);
|
||||
|
||||
self::assertStringContainsString('Keine aktiven Verbindungen.', $html);
|
||||
self::assertStringNotContainsString('<table', $html);
|
||||
self::assertStringContainsString('<table class="form-table" role="presentation">', $html);
|
||||
self::assertStringContainsString('<th scope="row">Rollen mit Zugriff</th>', $html);
|
||||
self::assertStringContainsString('<fieldset><legend class="screen-reader-text"><span>Rollen mit Zugriff</span></legend>', $html);
|
||||
self::assertStringContainsString('<p class="description">Nur Benutzer dieser Rollen können Claude verbinden.', $html);
|
||||
}
|
||||
|
||||
public function testDatesUseTheSiteDateAndTimeFormat(): void
|
||||
{
|
||||
$this->stubRender(['editor']);
|
||||
|
||||
$html = $this->renderTab([
|
||||
$this->connection(['uuid' => 'a', 'last_used' => null]),
|
||||
$this->connection(['uuid' => 'b', 'last_used' => 1790050000]),
|
||||
]);
|
||||
|
||||
self::assertStringContainsString('d.m.Y H:i@1790000000', $html);
|
||||
self::assertStringContainsString('d.m.Y@1790050000', $html);
|
||||
self::assertStringContainsString('<td class="column-last-used">–</td>', $html);
|
||||
}
|
||||
|
||||
public function testRenderWithoutConnectionsShowsAnEmptyStateRowLinkingToTheConnectionTab(): void
|
||||
{
|
||||
$this->stubRender(['administrator']);
|
||||
|
||||
$html = $this->renderTab([]);
|
||||
|
||||
self::assertStringContainsString('<tr class="no-items"><td class="colspanchange" colspan="5">', $html);
|
||||
self::assertStringContainsString('Noch keine Verbindung.', $html);
|
||||
self::assertStringContainsString('href="https://kunde.de/wp-admin/options-general.php?page=lit-mcp&tab=verbindung"', $html);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user