$abilities name, isDirect, readonly, destructive */ private function integration(string $id, array $abilities, bool $available = true): Integration { $mocks = []; foreach ($abilities as $spec) { [$name, $direct, $readonly] = $spec; $ability = Mockery::mock(Ability::class); $ability->allows('name')->andReturn($name); $ability->allows('label')->andReturn('Label ' . $name); $ability->allows('isDirect')->andReturn($direct); $ability->allows('annotations')->andReturn(['readonly' => $readonly, 'destructive' => $spec[3] ?? false]); $mocks[] = $ability; } $integration = Mockery::mock(Integration::class); $integration->allows('id')->andReturn($id); $integration->allows('isAvailable')->andReturn($available); $integration->allows('abilities')->andReturn($mocks); return $integration; } private function registry(): IntegrationRegistry { return new IntegrationRegistry( $this->integration('core', [['lit/guide', true, true], ['lit/site-status', true, true]]), $this->integration('media', [['lit/media-preview', true, true], ['lit/media-upload', false, false]]), $this->integration('divi', [['lit/divi-outline', true, true], ['lit/divi-update', true, false]]), $this->integration('pods', [['lit/pods-x', true, true]], false) ); } /** * @param array $option * @param mixed $closedBoxes the user's saved closed postboxes (false = never saved) */ private function render(array $option, ?IntegrationRegistry $registry = null, mixed $closedBoxes = false): string { Functions\stubEscapeFunctions(); Functions\stubTranslationFunctions(); Functions\when('settings_fields')->justReturn(null); Functions\when('submit_button')->alias(static function ($text = null, $type = 'primary', $name = 'submit'): void { echo ''; }); Functions\when('wp_nonce_field')->alias(static function (string $action, string $name): string { return ''; }); Functions\when('get_user_option')->alias( static fn (string $key) => $key === 'closedpostboxes_settings_page_lit-mcp' ? $closedBoxes : false ); Functions\when('selected')->alias( static fn ($a, $b = true, $echo = true): string => (string) $a === (string) $b ? ' selected="selected"' : '' ); Functions\when('checked')->alias( static fn ($a, $b = true, $echo = true): string => (string) $a === (string) $b ? ' checked="checked"' : '' ); ob_start(); (new ToolsTab($registry ?? $this->registry(), new ExposurePolicy($option)))->render(); return (string) ob_get_clean(); } private function select(string $html, string $key): string { $start = strpos($html, '', $update ); $outline = $this->row($html, 'lit/divi-outline'); self::assertStringNotContainsString('disabled>', $outline); self::assertStringNotContainsString('Nur lesen', $outline); } public function testCountsDirectToolsUnderTheSavedSettings(): void { $html = $this->render(['@group:divi' => 'disabled']); // guide, site-status, media-preview (divi disabled, media-upload indirect by default). self::assertStringContainsString('Direkt sichtbar: 3 Tools', $html); self::assertStringNotContainsString('Kontext', $html); self::assertLessThan(strpos($html, 'class="postbox'), strpos($html, 'Direkt sichtbar')); self::assertStringContainsString('

Direkt sichtbar: 3 Tools', $html); } public function testWarnsWhenMoreThan25ToolsAreDirect(): void { $many = []; for ($i = 1; $i <= 26; $i++) { $many[] = ['lit/tool-' . $i, true, true]; } $registry = new IntegrationRegistry($this->integration('wordpress', $many)); $warned = $this->render([], $registry); $notWarned = $this->render(['lit/tool-1' => 'indirect'], $registry); self::assertStringContainsString('Direkt sichtbar: 26 Tools', $warned); self::assertStringContainsString('Kontext', $warned); self::assertStringContainsString('

', $warned); self::assertStringNotContainsString('notice-warning', $notWarned); self::assertStringContainsString('

WordPress (26 Tools)', $warned); self::assertStringContainsString('Direkt sichtbar: 25 Tools', $notWarned); self::assertStringNotContainsString('Kontext', $notWarned); } public function testUnknownGroupsUseTheirIdAndEverythingIsEscaped(): void { $registry = new IntegrationRegistry($this->integration('shop', [['lit/shop-', true, true]])); $html = $this->render([], $registry); self::assertStringContainsString('

shop (1 Tool)', $html); self::assertStringNotContainsString('', $html); self::assertStringContainsString('lit/shop-<b>', $html); } public function testArtColumnSaysWhetherAToolReadsWritesOrDeletes(): void { $registry = new IntegrationRegistry($this->integration('media', [ ['lit/media-preview', true, true], ['lit/media-upload', false, false], ['lit/media-delete', false, false, true], ])); $html = $this->render([], $registry); self::assertStringContainsString('Art', $html); self::assertStringContainsString('liest', $this->row($html, 'lit/media-preview')); self::assertStringContainsString('schreibt', $this->row($html, 'lit/media-upload')); self::assertStringContainsString('löscht', $this->row($html, 'lit/media-delete')); self::assertStringNotContainsString('schreibt', $this->row($html, 'lit/media-delete')); } public function testToolCellShowsLabelAndTheNameAsMutedTextWithALabelledSelect(): void { $html = $this->render([]); $row = $this->row($html, 'lit/guide'); self::assertStringContainsString('Label lit/guide', $row); self::assertStringNotContainsString('', $row); self::assertMatchesRegularExpression('##', $row); preg_match('#for="([a-z0-9-]+)"#', $row, $m); self::assertStringContainsString('id="' . $m[1] . '"', $this->select($html, 'lit/guide')); } public function testIntroUsesPlainLanguageAndMentionsDiviOnlyWhenDiviIsAvailable(): void { $withDivi = $this->render([]); $withoutDivi = $this->render([], new IntegrationRegistry($this->integration('core', [['lit/guide', true, true]]))); self::assertStringContainsString('Claude sieht das Tool sofort.', $withDivi); self::assertStringContainsString('Claude findet es bei Bedarf über die Tool-Suche.', $withDivi); self::assertStringNotContainsString('Meta-Tools', $withDivi); self::assertStringNotContainsString('discover', $withDivi); self::assertStringContainsString('Divi-Code-Modul', $withDivi); self::assertStringNotContainsString('Divi-Code-Modul', $withoutDivi); } public function testHasASubmitButtonAboveAndBelowTheGroups(): void { $html = $this->render([]); self::assertSame(2, substr_count($html, 'Speichern')); self::assertLessThan(strpos($html, 'class="postbox'), strpos($html, 'name="submit-top"')); self::assertGreaterThan(strrpos($html, 'class="postbox'), strrpos($html, 'name="submit"')); } public function testGroupsAreWordPressPostboxesWithAnAccessibleToggle(): void { $html = $this->render([]); $divi = $this->group($html, 'Divi'); self::assertStringContainsString('
', $divi); self::assertStringContainsString('
', $divi); self::assertStringContainsString('