justReturn(true); Functions\when('delete_option')->justReturn(true); } private function ability( UsageReport $report, ?DesignSystemWriter $writer = null, ?SnapshotStore $snapshots = null, bool $unfiltered = true, array $stored = [] ): DiviPresetsDeleteAbility { Functions\when('current_user_can')->justReturn(true); if ($writer === null) { $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('presets')->andReturn($stored === [] ? PresetBookTest::stored() : $stored); $writer->allows('canUseVisualBuilder')->andReturn(true); } $usages = Mockery::mock(GlobalUsages::class); $usages->allows('reportFor')->andReturn($report); return new DiviPresetsDeleteAbility( $writer, new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), new FakeChangeLog(3)), $usages, static fn (): bool => $unfiltered ); } private static function unused(): UsageReport { return new UsageReport(0, [], 0, 0, [], 0, false); } public function testUnusedPresetIsDeletedAfterConfirm(): void { $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('canUseVisualBuilder')->andReturn(true); $writer->expects('savePresets') ->with(Mockery::on(static function (array $rest): bool { $ids = array_column($rest['module'][0]['items'], 'id'); return $ids === ['aaaaaaaaaa', 'cccccccccc']; }), true) ->andReturnUsing(static function (): array { $stored = PresetBookTest::stored(); unset($stored['module']['divi/button']['items']['bbbbbbbbbb']); return $stored; }); $snapshots = Mockery::mock(SnapshotStore::class); $snapshots->expects('save')->andReturn('presets-9'); $snapshots->expects('attachLog'); $ability = $this->ability(self::unused(), $writer, $snapshots); $input = ['items' => [['module' => 'button', 'preset' => 'bbbbbbbbbb']]]; $dry = self::assertToolError($ability->execute($input), 'confirm_required'); self::assertSame('removed', $dry['diff'][0]['action']); $result = $ability->execute($input + ['confirm' => true, 'design_rev' => $dry['design_rev']]); self::assertSame('presets-9', $result['snapshot_id']); } public function testPresetInUseIsRefused(): void { $used = new UsageReport(2, [['post_id' => 3, 'title' => 'Start', 'status' => 'publish', 'type' => 'page']], 1, 1, [], 0, false); $error = self::assertToolError( $this->ability($used)->execute(['items' => [['module' => 'button', 'preset' => 'CTA primär']]]), 'in_use' ); self::assertSame(3, $error['usages'][0]['post_id']); self::assertStringContainsString('lit/divi-design-detach', $error['hint']); self::assertStringNotContainsString('4b', $error['hint'], 'the tool exists now'); } public function testNestedUseInAnotherPresetCounts(): void { $error = self::assertToolError( $this->ability(self::unused())->execute(['items' => [['group' => 'font-body', 'preset' => 'Lesetext']]]), 'in_use' ); self::assertSame('preset', $error['usages'][0]['type']); self::assertSame('dddddddddd', $error['usages'][0]['preset_id']); } public function testDeletingTheDefaultClearsIt(): void { $dry = self::assertToolError( $this->ability(self::unused())->execute(['items' => [['module' => 'text', 'preset' => 'Fließtext']]]), 'confirm_required' ); self::assertSame(['removed', 'default_cleared'], array_column($dry['diff'], 'action')); } /** Final review I-2: deleting saves the whole inventory – other presets' HTML/CSS would be stripped. */ public function testEditorIsRefusedWhenOtherPresetsCarryMarkup(): void { $input = ['items' => [['module' => 'button', 'preset' => 'cccccccccc']]]; $markup = PresetBookTest::storedWithMarkup(); $editor = self::assertToolError( $this->ability(self::unused(), null, null, false, $markup)->execute($input), 'forbidden' ); self::assertStringContainsString('unfiltered_html', $editor['message']); self::assertToolError($this->ability(self::unused(), null, null, true, $markup)->execute($input), 'confirm_required'); self::assertToolError($this->ability(self::unused(), null, null, false)->execute($input), 'confirm_required'); } /** @return list The default flags of a PresetUsageQuery's targets. */ private static function defaultFlags(PresetUsageQuery $query): array { $targets = (new ReflectionProperty(PresetUsageQuery::class, 'targets'))->getValue($query); return array_column($targets, 'default'); } /** Final review I-6 (spec §6.4): implicit use of a default preset does not block; the dry run lists it. */ public function testImplicitUseOfADefaultPresetIsListedNotBlocking(): void { $used = new UsageReport(1, [['post_id' => 3, 'title' => 'Start', 'status' => 'publish', 'type' => 'page']], 0, 1, [], 0, false); Functions\when('current_user_can')->justReturn(true); $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('presets')->andReturn(PresetBookTest::stored()); $writer->allows('canUseVisualBuilder')->andReturn(true); $usages = Mockery::mock(GlobalUsages::class); $usages->expects('reportFor') ->with(Mockery::on(static fn (PresetUsageQuery $q): bool => self::defaultFlags($q) === [false]), false) ->andReturn(self::unused()); $usages->expects('reportFor') ->with(Mockery::on(static fn (PresetUsageQuery $q): bool => self::defaultFlags($q) === [true])) ->andReturn($used); $ability = new DiviPresetsDeleteAbility( $writer, new DesignChange(Mockery::mock(SnapshotStore::class), FakeChangeLog::forbidden()), $usages, static fn (): bool => true ); $dry = self::assertToolError( $ability->execute(['items' => [['module' => 'text', 'preset' => 'Fließtext']]]), 'confirm_required' ); self::assertSame(3, $dry['affected'][0]['post_id']); self::assertSame(1, $dry['affected_count']); self::assertContains('Module ohne eigenes Preset verwenden danach das Divi-Standardaussehen.', $dry['warnings']); } public function testUnknownPreset(): void { self::assertToolError( $this->ability(self::unused())->execute(['items' => [['module' => 'button', 'preset' => 'Fehlt']]]), 'validation_failed' ); } }