justReturn(true); } private function ability(PageRewriter $pages): DiviPresetsAssignAbility { $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('canUseVisualBuilder')->andReturn(true); return new DiviPresetsAssignAbility( $writer, $pages, static fn (): PresetAssignment => new PresetAssignment( static fn (): PresetBook => PresetBook::fromDivi(PresetBookTest::stored()), static fn (string $module): array => $module === 'divi/text' ? ['content.decoration.bodyFont' => 'divi/font-body'] : [], static fn (string $id): bool => preg_match('/^[0-9a-z]{10}$/', $id) === 1 ) ); } /** A PageRewriter that must not be asked at all. */ private static function untouched(): PageRewriter&MockInterface { $pages = Mockery::mock(PageRewriter::class); $pages->expects('preview')->never(); $pages->expects('apply')->never(); return $pages; } private static function moduleOf(ModuleUsageQuery $query): string { return (new ReflectionProperty(ModuleUsageQuery::class, 'module'))->getValue($query); } private static function onlyWithOf(AssignRewrite $rewrite): ?string { return (new ReflectionProperty(AssignRewrite::class, 'onlyWithPresetId'))->getValue($rewrite); } public function testNeedsEditThemeOptionsAndTheVisualBuilder(): void { $ability = $this->ability(self::untouched()); Functions\when('current_user_can')->justReturn(false); $input = ['module' => 'button', 'preset' => 'CTA primär']; self::assertToolError($ability->permission($input), 'forbidden'); self::assertToolError($ability->execute($input), 'forbidden'); } public function testUnknownPresetIsRefusedBeforeAnyPageIsRead(): void { $result = $this->ability(self::untouched())->execute(['module' => 'button', 'preset' => 'Fehlt']); self::assertToolError($result, 'validation_failed'); self::assertArrayHasKey('preset', self::errorFields($result)); } public function testOnlyWithAndOnlyWithoutPresetExcludeEachOther(): void { $result = $this->ability(self::untouched())->execute([ 'module' => 'button', 'preset' => 'CTA primär', 'only_without_preset' => true, 'only_with_preset' => 'aaaaaaaaaa', ]); self::assertToolError($result, 'validation_failed'); self::assertSame( ['only_with_preset' => 'nicht zusammen mit only_without_preset'], self::errorFields($result) ); } public function testNullAndMissingAssignmentPointToDetach(): void { $ability = $this->ability(self::untouched()); $null = $ability->execute(['module' => 'button', 'preset' => null]); self::assertToolError($null, 'validation_failed'); self::assertStringContainsString('lit/divi-design-detach', self::errorFields($null)['preset']); $groupNull = $ability->execute(['module' => 'text', 'group_presets' => ['font-body' => null]]); self::assertStringContainsString( 'lit/divi-design-detach', self::errorFields($groupNull)['group_presets.font-body'] ); $missing = $ability->execute(['module' => 'button']); self::assertSame('preset oder group_presets angeben', self::errorFields($missing)['preset']); } public function testEmptyStringAssignmentPointsToDetachToo(): void { $ability = $this->ability(self::untouched()); $empty = $ability->execute(['module' => 'button', 'preset' => '', 'confirm' => true]); self::assertToolError($empty, 'validation_failed'); self::assertStringContainsString('lit/divi-design-detach', self::errorFields($empty)['preset']); $group = $ability->execute(['module' => 'text', 'group_presets' => ['font-body' => ''], 'confirm' => true]); self::assertToolError($group, 'validation_failed'); self::assertStringContainsString( 'lit/divi-design-detach', self::errorFields($group)['group_presets.font-body'] ); } public function testInputSchemaForbidsEmptyPresetStrings(): void { $schema = $this->ability(self::untouched())->inputSchema()['properties']['preset']; self::assertSame(1, $schema['minLength']); } public function testBadModuleFiltersAndPostIdsAreCollected(): void { $ability = $this->ability(self::untouched()); $fields = self::errorFields($ability->execute([ 'module' => 'Button!', 'preset' => 'CTA primär', 'post_ids' => [0], 'clear_overrides' => 'ja', ])); self::assertSame(['module', 'post_ids', 'clear_overrides'], array_keys($fields)); $unknown = self::errorFields($ability->execute([ 'module' => 'button', 'preset' => 'CTA primär', 'only_with_preset' => 'Fehlt', ])); self::assertSame('unbekanntes Modul-Preset divi/button „Fehlt“', $unknown['only_with_preset']); } public function testDryRunListsThePagesAndWritesNothing(): void { $pages = Mockery::mock(PageRewriter::class); $pages->expects('preview') ->with( Mockery::on(static fn (ModuleUsageQuery $query): bool => self::moduleOf($query) === 'divi/button'), Mockery::on(static fn (Closure $rewrite): bool => $rewrite() instanceof AssignRewrite), null ) ->andReturn(PageRewriteReportTest::preview(2)); $pages->expects('apply')->never(); $dry = self::assertToolError( $this->ability($pages)->execute(['module' => 'button', 'preset' => ['CTA primär', 'cccccccccc']]), 'confirm_required' ); self::assertSame([12, 13], array_column($dry['pages'], 'post_id')); self::assertSame(2, $dry['pages_count']); self::assertStringStartsWith('Zuweisung an divi/button – betroffen (Seiten: 2, Module: 2).', $dry['message']); self::assertStringContainsString('confirm: true', $dry['message']); } public function testEmptyDryRunIsNothingToDo(): void { $pages = Mockery::mock(PageRewriter::class); $pages->expects('preview')->andReturn(PageRewriteReportTest::preview(0)); $pages->expects('apply')->never(); $result = $this->ability($pages)->execute(['module' => 'button', 'preset' => 'CTA primär']); self::assertTrue($result['ok']); self::assertSame('Zuweisung an divi/button – keine Seite betroffen, nichts zu tun.', $result['summary']); self::assertSame([[], 0], [$result['saved'], $result['remaining']]); } public function testConfirmWritesAndReportsWhatRemains(): void { $pages = Mockery::mock(PageRewriter::class); $pages->expects('preview')->never(); $pages->expects('apply') ->with( 'lit/divi-presets-assign', Mockery::on(static fn (ModuleUsageQuery $query): bool => self::moduleOf($query) === 'divi/button'), Mockery::on(static fn (Closure $rewrite): bool => self::onlyWithOf($rewrite()) === 'aaaaaaaaaa'), [12, 13], 'Zuweisung an divi/button' ) ->andReturn(PageRewriteReportTest::applied(2, 3)); $result = $this->ability($pages)->execute([ 'module' => 'divi/button', 'preset' => ['bbbbbbbbbb', 'cccccccccc'], 'only_with_preset' => 'CTA primär', 'post_ids' => [12, 13], 'confirm' => true, ]); self::assertTrue($result['ok']); self::assertSame(3, $result['remaining']); self::assertSame([900, 901], array_column($result['saved'], 'revision_id')); self::assertStringContainsString('Noch 3 Seiten offen', $result['summary']); self::assertStringContainsString('confirm: true', $result['summary']); } public function testSchemaAndMeta(): void { $ability = $this->ability(self::untouched()); $schema = $ability->inputSchema(); self::assertSame('lit/divi-presets-assign', $ability->name()); self::assertFalse($ability->isDirect()); self::assertSame(['readonly' => false, 'destructive' => false, 'idempotent' => true], $ability->annotations()); self::assertSame(['module'], $schema['required']); self::assertFalse($schema['additionalProperties']); self::assertSame(PresetAssignment::MAX_STACK, $schema['properties']['preset']['maxItems']); self::assertArrayNotHasKey('design_rev', $schema['properties']); self::assertSame(['saved', 'remaining', 'skipped'], array_slice($ability->outputSchema()['required'], -3)); self::assertStringContainsString('gewinnt das letzte Preset im Stapel', $ability->description()); } }