fix: layout-edit dry runs name only editable live templates (ruling Q22)
ThemeBuilderEdits::showing() splits the live templates that show a layout into editable ones (named under templates) and a hidden_templates count; confirm is still decided on all of them, so a layout shown only by uneditable templates still needs confirm. The live split is read once per write for the confirm check and the field warnings, and field warnings now also survive a non-confirm refusal such as the P9 backup refusal (M-12 a). replace-text passes hidden_templates on; the write schema lists it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -194,6 +194,58 @@ final class DiviWriterThemeBuilderTest extends TestCase
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
/** Ruling Q22: replace-text on a layout shown only by a template the user may not edit names nothing. */
|
||||
public function testReplaceTextOnALayoutOfAHiddenTemplateLeaksNothing(): void
|
||||
{
|
||||
Functions\when('get_post_type_object')->justReturn(
|
||||
(object) ['cap' => (object) ['edit_posts' => 'edit_pages', 'edit_others_posts' => 'edit_others_pages']]
|
||||
);
|
||||
Functions\when('current_user_can')->alias(static fn (string $cap, mixed $id = null): bool => $id !== 1065);
|
||||
$access = ThemeBuilderFixtures::access(
|
||||
[],
|
||||
true,
|
||||
ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1065, ['enabled' => true])])
|
||||
);
|
||||
$loader = ThemeBuilderFixtures::loader($access);
|
||||
$writer = DiviWriterFixture::writer($this->saved, $this->failures, false, $loader, null, new ThemeBuilderEdits($access));
|
||||
$ability = new DiviReplaceTextAbility(DiviFixtures::schema(), $loader, $writer, Mockery::mock(\wpdb::class)->makePartial());
|
||||
|
||||
$result = $ability->execute([
|
||||
'search' => 'Globaler Header der Standard-Vorlage',
|
||||
'replace' => 'Kopfzeile',
|
||||
'dry_run' => false,
|
||||
'confirm' => true,
|
||||
'targets' => [['post_id' => 1061, 'tree_rev' => Rev::of((string) $this->header->post_content)]],
|
||||
]);
|
||||
|
||||
self::assertIsArray($result, $result instanceof \WP_Error ? $result->get_error_message() : '');
|
||||
$entry = $result['results'][0];
|
||||
self::assertFalse($entry['ok']);
|
||||
self::assertStringContainsString('Theme-Builder-Layout 1061', $entry['error']);
|
||||
self::assertSame([], $entry['templates']);
|
||||
self::assertSame(1, $entry['hidden_templates']);
|
||||
self::assertStringNotContainsString('1065', (string) json_encode($entry));
|
||||
self::assertStringNotContainsString('Standard-Website-Template', (string) json_encode($entry));
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
/** Review M-12 (a): the field warnings stay in the answer when the live edit is refused for its backup (P9). */
|
||||
public function testFieldWarningsSurviveABackupRefusal(): void
|
||||
{
|
||||
Functions\when('wp_revisions_enabled')->justReturn(false);
|
||||
$field = ['nodes' => [['type' => 'text', 'content' => '{{field:telefon}}']], 'confirm' => true];
|
||||
$live = ThemeBuilderFixtures::template(1066, ['enabled' => true, 'use_on' => ['singular:post_type:lit_person:all']]);
|
||||
$visible = new VisibleEdits(static fn (WP_Post $post): int => 1071, static fn (): bool => false);
|
||||
|
||||
$refused = (new DiviInsertAbility($this->writer([$live], $visible, self::unknownFields())))
|
||||
->execute($this->insert($this->body, $field));
|
||||
|
||||
self::assertToolError($refused, 'unsupported');
|
||||
self::assertStringContainsString('Papierkorb', self::clientError($refused)['message']);
|
||||
self::assertStringContainsString('„telefon“', implode(' ', $refused->get_error_data()['warnings']));
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
private static function unknownFields(): DynamicFields
|
||||
{
|
||||
$fields = Mockery::mock(DynamicFields::class);
|
||||
|
||||
@@ -92,8 +92,11 @@ final class ThemeBuilderEditsTest extends TestCase
|
||||
{
|
||||
$page = self::page(ThemeBuilderFixtures::HEADER, 'header');
|
||||
|
||||
self::assertSame([], self::edits([ThemeBuilderFixtures::template(1065)])->showing($page));
|
||||
self::assertCount(1, self::edits([ThemeBuilderFixtures::template(1065, ['enabled' => true])])->showing($page));
|
||||
self::assertSame(['templates' => [], 'hidden' => 0], self::edits([ThemeBuilderFixtures::template(1065)])->showing($page));
|
||||
self::assertCount(
|
||||
1,
|
||||
self::edits([ThemeBuilderFixtures::template(1065, ['enabled' => true])])->showing($page)['templates']
|
||||
);
|
||||
}
|
||||
|
||||
public function testLayoutsOfDisabledOrDetachedTemplatesSaveAtOnce(): void
|
||||
@@ -117,6 +120,53 @@ final class ThemeBuilderEditsTest extends TestCase
|
||||
self::assertToolError($edits->prepare($page, []), 'confirm_required');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling Q22 (Q21 pattern): a live template the user may not edit is only counted – never its ID, title or
|
||||
* conditions – but still decides that confirm is needed.
|
||||
*/
|
||||
public function testLiveTemplatesTheUserMayNotEditAreOnlyCounted(): void
|
||||
{
|
||||
Functions\when('current_user_can')->alias(static fn (string $cap, mixed $id = null): bool => $id !== 1067);
|
||||
$page = self::page(ThemeBuilderFixtures::BODY, 'body');
|
||||
$mine = ThemeBuilderFixtures::template(1066, ['enabled' => true]);
|
||||
$theirs = ThemeBuilderFixtures::template(1067, ['enabled' => true, 'title' => 'Geheim', 'use_on' => ['singular:post_type:page:id:4711']]);
|
||||
|
||||
$dry = self::edits([$mine, $theirs])->prepare($page, []);
|
||||
|
||||
self::assertToolError($dry, 'confirm_required');
|
||||
$data = $dry->get_error_data();
|
||||
self::assertSame([1066], array_column($data['templates'], 'id'));
|
||||
self::assertSame(1, $data['hidden_templates']);
|
||||
self::assertStringContainsString('2 aktive Vorlagen, davon 1 ohne Berechtigung', self::clientError($dry)['message']);
|
||||
$json = (string) json_encode($data) . self::clientError($dry)['message'];
|
||||
self::assertStringNotContainsString('1067', $json);
|
||||
self::assertStringNotContainsString('Geheim', $json);
|
||||
self::assertStringNotContainsString('4711', $json);
|
||||
}
|
||||
|
||||
/** Ruling Q22: a layout shown only by templates the user may not edit still needs confirm (fail closed, F1/Q9). */
|
||||
public function testALayoutShownOnlyByHiddenTemplatesStillNeedsConfirm(): void
|
||||
{
|
||||
Functions\when('current_user_can')->alias(static fn (string $cap, mixed $id = null): bool => $id !== 1067);
|
||||
$page = self::page(ThemeBuilderFixtures::BODY, 'body');
|
||||
$edits = self::edits([ThemeBuilderFixtures::template(1067, ['enabled' => true])]);
|
||||
|
||||
$dry = $edits->prepare($page, []);
|
||||
self::assertToolError($dry, 'confirm_required');
|
||||
self::assertSame([], $dry->get_error_data()['templates']);
|
||||
self::assertSame(1, $dry->get_error_data()['hidden_templates']);
|
||||
|
||||
$confirmed = $edits->prepare($page, ['confirm' => true]);
|
||||
self::assertSame(SaveScope::ThemeBuilderLayout, $confirmed['scope']);
|
||||
self::assertSame(['templates' => [], 'hidden_templates' => 1], $confirmed['details']);
|
||||
|
||||
$own = Errors::confirmRequired('3 Knoten löschen? Nichts wurde gelöscht.', ['ids' => ['s0']]);
|
||||
$merged = $edits->notice($page, $own);
|
||||
self::assertStringContainsString('sichtbar', self::clientError($merged)['message']);
|
||||
self::assertSame([], $merged->get_error_data()['templates']);
|
||||
self::assertSame(1, $merged->get_error_data()['hidden_templates']);
|
||||
}
|
||||
|
||||
public function testNoticeExtendsAToolsOwnConfirm(): void
|
||||
{
|
||||
$page = self::page(ThemeBuilderFixtures::BODY, 'body');
|
||||
|
||||
Reference in New Issue
Block a user