justReturn(false); Functions\when('post_type_exists')->justReturn(false); Functions\when('get_post')->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 { unset($_GET['paged']); parent::tearDown(); } /** * @param array $filters * @param array $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], 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 = $this->table(['ability' => 'lit/content-update'], [], new LogObjects(), $log); $table->prepare_items(); self::assertSame($rows, $table->items); self::assertSame(['total_items' => 120, 'per_page' => 30], $table->pagination_args()); } public function testPrepareItemsDefaultsToTwentyPerPage(): void { $log = Mockery::mock(ChangeLog::class); $log->expects('query')->with(['ability' => '', 'user_id' => 0], 20, 0)->andReturn(['rows' => [], 'total' => 0]); $table = $this->table([], [], null, $log); $table->prepare_items(); self::assertSame(['total_items' => 0, 'per_page' => 20], $table->pagination_args()); } public function testColumnsPutTheChangeFirstAsTheUnhideablePrimaryColumn(): void { $table = $this->table(); 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 testChangeColumnShowsTheSummaryAndTheToolsHumanLabel(): void { $table = $this->table([], ['lit/media-upload-from-url' => 'Datei von URL ']); $html = $this->column($table, ['summary' => 'Datei X importiert', 'ability' => 'lit/media-upload-from-url', 'result' => 'ok'], 'title'); self::assertStringContainsString('Datei <b>X</b> importiert', $html); self::assertStringContainsString( 'Datei von URL <importieren>', $html ); self::assertStringNotContainsString('lit-mcp-badge', $html); } public function testChangeColumnFallsBackToTheToolNameForAnUnknownTool(): void { $html = $this->column($this->table(), ['summary' => 'x', 'ability' => 'lit/gone<', 'result' => 'ok'], 'title'); self::assertStringContainsString('lit/gone<', $html); } /** @dataProvider notOk */ public function testANotOkResultGetsABadgeAndAMarkedRow(string $result, string $class, string $word): void { $table = $this->table(); $html = $this->column($table, ['summary' => 'x', 'ability' => 'lit/a', 'result' => $result], 'title'); self::assertStringContainsString('' . $word . '', $html); ob_start(); $table->single_row(['result' => $result, 'summary' => 'x', 'ability' => 'lit/a']); self::assertStringStartsWith('', (string) ob_get_clean()); } /** @return list> */ public static function notOk(): array { return [ ['partial', 'warning', 'teilweise'], ['failed', 'error', 'fehlgeschlagen'], ['kaputt', 'error', 'unbekannt'], ]; } public function testAnOkRowIsNotMarked(): void { ob_start(); $this->table()->single_row(['result' => 'ok', 'summary' => 'x', 'ability' => 'lit/a']); self::assertStringStartsWith('', (string) ob_get_clean()); } 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')->alias(static fn (string $cap, int $id = 0): bool => $cap === 'edit_post' && $id === 12); $table = $this->table([], [], $objects); $both = $this->rowActions($table, ['object_type' => 'page', 'object_id' => '12', 'revision_id' => '811']); self::assertStringContainsString('Revision ansehen', $both); self::assertStringContainsString('Objekt öffnen', $both); $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 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); $html = $this->rowActions($this->table(), ['object_type' => 'x', 'object_id' => '1', 'revision_id' => '811']); self::assertStringNotContainsString('revision.php', $html); } public function testObjectColumnUsesTheHumanObjectDescription(): void { $objects = Mockery::mock(LogObjects::class); $objects->expects('describe')->with('page', '12')->andReturn(['html' => 'Seite: X', 'url' => 'u']); self::assertSame('Seite: X', $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 ']); $client = 'lit-mcp-integration-test-client-