*/
private array $stored;
private SnapshotStore $snapshots;
protected function setUp(): void
{
parent::setUp();
$this->stored = PresetBookTest::stored();
$this->snapshots = Mockery::mock(SnapshotStore::class);
Functions\when('current_user_can')->justReturn(true);
}
private function ability(bool $unfiltered = true, ?GlobalUsages $usages = null, ?DiviPageLoader $loader = null, ?DesignSystemWriter $writer = null): DiviPresetsSetAbility
{
$writer ??= $this->savingWriter();
$usages ??= Mockery::mock(GlobalUsages::class);
$usages->allows('reportFor')->andReturn(new UsageReport(0, [], 0, 0, [], 0, false));
return $this->build($writer, $usages, $loader, $unfiltered);
}
private function savingWriter(): DesignSystemWriter
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('savePresets')->andReturnUsing(function (array $rest, bool $mayRemove): array {
$data = ['module' => [], 'group' => []];
foreach ($rest as $kind => $records) {
foreach ($records as $record) {
$first = $record['items'][0] ?? null;
$name = $kind === 'group' ? ($first['groupName'] ?? 'x') : ($first['moduleName'] ?? 'x');
$data[$kind][$name] = ['default' => $record['default'], 'items' => array_column($record['items'], null, 'id')];
}
}
return $this->stored = $data;
});
return $writer;
}
private function build(DesignSystemWriter $writer, GlobalUsages $usages, ?DiviPageLoader $loader, bool $unfiltered): DiviPresetsSetAbility
{
$composer = static fn (): PresetComposer => new PresetComposer(
ComposeFixture::composer($unfiltered),
new StoredNodeGuard(ComposeFixture::cleaner($unfiltered), ComposeFixture::references(),
ComposeFixture::sanitizer($unfiltered)),
static fn (string $module, array $attrs): array => ['all' => array_map(
static fn (mixed $v): mixed => is_array($v) ? array_diff_key($v, ['innerContent' => 1]) : $v,
$attrs
), 'style' => [], 'render' => []],
static fn (string $module): array => ['module.decoration.spacing' => 'divi/spacing'],
'5.15.1',
static fn (): int => 1
);
return new DiviPresetsSetAbility(
$writer,
new DesignChange($this->snapshots, new FakeChangeLog(5)),
$usages,
$loader ?? new DiviPageLoader(DiviFixtures::schema()),
$composer
);
}
private static function newButton(array $extra = []): array
{
return ['items' => [$extra + ['module' => 'button', 'name' => 'CTA dunkel',
'attrs' => ['button.decoration.background' => ['desktop' => ['color' => '#111111']]]]]];
}
public function testDryRunThenConfirmCreatesThePreset(): void
{
$ability = $this->ability();
$dry = self::assertToolError($ability->execute(self::newButton()), 'confirm_required');
self::assertSame('added', $dry['diff'][0]['action']);
self::assertStringContainsString('Presets: 1 angelegt.', $dry['message']);
$this->snapshots->expects('save')->andReturn('presets-1');
$this->snapshots->expects('attachLog');
$result = $ability->execute(self::newButton() + ['confirm' => true, 'design_rev' => $dry['design_rev']]);
self::assertTrue($result['ok']);
self::assertSame('presets-1', $result['snapshot_id']);
$names = array_column(PresetBook::fromDivi($this->stored)->items(new PresetScope('module', 'divi/button')), 'name');
self::assertContains('CTA dunkel', $names);
}
public function testExistingNameIsRefusedForANewPreset(): void
{
$result = self::assertToolError($this->ability()->execute(['items' => [
['module' => 'button', 'name' => 'CTA primär'],
]]), 'validation_failed');
self::assertStringContainsString('aaaaaaaaaa', $result['fields']['items[0].name']);
}
public function testChangingAndMakingDefault(): void
{
$dry = self::assertToolError($this->ability()->execute(['items' => [[
'module' => 'button', 'preset' => 'CTA primär', 'make_default' => true,
'attrs' => ['button.decoration.background' => ['desktop' => ['color' => '#222222']]],
]]]), 'confirm_required');
self::assertSame(['changed', 'default_set'], array_column($dry['diff'], 'action'));
}
public function testUnknownPresetAndBadItemsAreCollected(): void
{
$result = self::assertToolError($this->ability()->execute(['items' => [
['module' => 'button', 'preset' => 'Fehlt'],
['name' => 'ohne Modul'],
]]), 'validation_failed');
self::assertArrayHasKey('items[0].preset', $result['fields']);
self::assertArrayHasKey('items[1]', $result['fields']);
}
/** Review Focus 1 (final review I-1): a catalog-known builder-only path, refused for editors only. */
public function testEditorWithoutUnfilteredHtmlIsRefusedForBuilderOnlyFields(): void
{
$input = ['items' => [[
'module' => 'button', 'name' => 'Mit HTML',
'attrs' => ['module.advanced.html' => ['desktop' => ['elementType' => 'div']]],
]]];
$editor = self::assertToolError($this->ability(false)->execute($input), 'validation_failed');
self::assertArrayHasKey('items[0].attrs.module.advanced.html', $editor['fields']);
self::assertStringContainsString('Divi-Builder', $editor['fields']['items[0].attrs.module.advanced.html']);
self::assertStringContainsString('unfiltered_html', $editor['fields']['items[0].attrs.module.advanced.html']);
self::assertToolError($this->ability(true)->execute($input), 'confirm_required');
}
public function testFromNodeReadsThePage(): void
{
$content = ''
. ''
. '';
DiviFixtures::givenPost($content);
$loader = new DiviPageLoader(DiviFixtures::schema());
$dry = self::assertToolError($this->ability(true, null, $loader)->execute(['items' => [[
'module' => 'button', 'name' => 'Aus Seite', 'from_node' => ['post_id' => 7, 'id' => 's0.r0.c0.m0'],
]]]), 'confirm_required');
self::assertSame('added', $dry['diff'][0]['action']);
}
public function testNamesAreCleanedAndLimited(): void
{
$ability = $this->ability();
$dry = self::assertToolError($ability->execute(self::newButton(['name' => 'Fett Name'])), 'confirm_required');
$this->snapshots->expects('save')->andReturn('presets-1');
$this->snapshots->expects('attachLog');
$res = $ability->execute(self::newButton(['name' => 'Fett Name']) + ['confirm' => true, 'design_rev' => $dry['design_rev']]);
self::assertTrue($res['ok']);
$names = array_column(PresetBook::fromDivi($this->stored)->items(new PresetScope('module', 'divi/button')), 'name');
self::assertContains('Fett Name', $names);
self::assertNotContains('Fett Name', $names);
$long = self::assertToolError($this->ability()->execute(self::newButton(['name' => str_repeat('n', 101)])), 'validation_failed');
self::assertStringContainsString('1–100 Zeichen', $long['fields']['items[0].name']);
$empty = self::assertToolError($this->ability()->execute(['items' => [
['module' => 'button', 'preset' => 'CTA primär', 'rename' => ''],
]]), 'validation_failed');
self::assertStringContainsString('1–100 Zeichen', $empty['fields']['items[0].rename']);
}
public function testNameAndRenameAreExclusive(): void
{
$ability = $this->ability();
$new = self::assertToolError($ability->execute(['items' => [
['module' => 'button', 'name' => 'Neu', 'rename' => 'x'],
]]), 'validation_failed');
self::assertStringContainsString('nur zusammen mit preset', $new['fields']['items[0].rename']);
$change = self::assertToolError($ability->execute(['items' => [
['module' => 'button', 'preset' => 'CTA primär', 'name' => 'CTA primär'],
]]), 'validation_failed');
self::assertStringContainsString('rename', $change['fields']['items[0].name']);
}
public function testRenameToAnotherPresetsNameIsRefused(): void
{
$result = self::assertToolError($this->ability()->execute(['items' => [
['module' => 'button', 'preset' => 'CTA primär', 'rename' => 'Doppelt'],
]]), 'validation_failed');
self::assertStringContainsString('bbbbbbbbbb', $result['fields']['items[0].rename']);
}
public function testNothingIsWrittenWhenOneItemHasErrors(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->shouldNotReceive('savePresets');
$this->snapshots->shouldNotReceive('save');
$ability = $this->ability(true, null, null, $writer);
$result = $ability->execute(['items' => [
['module' => 'button', 'name' => 'Gut',
'attrs' => ['button.decoration.background' => ['desktop' => ['color' => '#333333']]]],
['module' => 'button', 'name' => 'Schlecht', 'attrs' => ['button.innerContent' => ['desktop' => ['text' => 'Hallo']]]],
], 'confirm' => true, 'design_rev' => 'egal']);
self::assertContains(Errors::codeOf($result), ['validation_failed', 'invalid_attr_path']);
self::assertNotEmpty(array_filter(
array_keys(Errors::fieldsOf($result)),
static fn (string $key): bool => str_starts_with($key, 'items[1]')
));
}
public function testNeedsEditThemeOptionsAndTheVisualBuilder(): void
{
Functions\when('current_user_can')->justReturn(false);
self::assertToolError($this->ability()->permission(self::newButton()), 'forbidden');
}
public function testSchemaAndMeta(): void
{
$ability = $this->ability();
self::assertSame('lit/divi-presets-set', $ability->name());
self::assertFalse($ability->isDirect());
self::assertSame(DiviPresetsSetAbility::MAX_ITEMS, $ability->inputSchema()['properties']['items']['maxItems']);
self::assertSame(['diff', 'design_rev'], array_slice($ability->outputSchema()['required'], -2));
}
}