fix: identical means identical bytes – text parts count, pages and posts only (ruling Q25)
- Fingerprint::content() includes non-whitespace text parts (raw HTML between blocks, inner HTML of core blocks) and opaque blocks with all their bytes; structure() unchanged; find-similar "identical" follows - make-global mode 2 also compares Fingerprint::canonical() bytes of each node - sources and targets: post types page and post only (POST_TYPES) - a rolled-back page N>1 keeps DiviSaver's "Revision X bleibt erhalten" - guide: escaped divi/global-layout re-expanded by Divi's do_blocks in shortcode-module and WooCommerce text output documented as residual risk 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:
@@ -68,4 +68,68 @@ final class FingerprintTest extends TestCase
|
||||
self::assertNotSame(Fingerprint::structure($a), Fingerprint::structure($b));
|
||||
self::assertNotSame(Fingerprint::structure($a), Fingerprint::structure($c));
|
||||
}
|
||||
|
||||
/** The section of 1055 with $extra inserted at the end of its column (review of Task 15, ruling Q25). */
|
||||
private static function sectionWith(string $extra): Block
|
||||
{
|
||||
$markup = DiviFixtures::markup('stage3/library--library-abschnitt.html');
|
||||
$at = strrpos($markup, '<!-- /wp:divi/column -->');
|
||||
$markup = substr($markup, 0, (int) $at) . $extra . substr($markup, (int) $at);
|
||||
|
||||
return LibraryContent::roots(LibraryContent::load(new Parser(), $markup))[0];
|
||||
}
|
||||
|
||||
/** @return array<string, array{string, string}> */
|
||||
public static function visibleExtras(): array
|
||||
{
|
||||
return [
|
||||
'raw html between blocks' => ['<p>ONLY ON B</p>', '<p>ONLY ON C</p>'],
|
||||
'core paragraph' => [
|
||||
'<!-- wp:paragraph --><p>ONLY ON B</p><!-- /wp:paragraph -->',
|
||||
'<!-- wp:paragraph --><p>ONLY ON C</p><!-- /wp:paragraph -->',
|
||||
],
|
||||
'divi 4 shortcode text' => ['[et_pb_text]ONLY ON B[/et_pb_text]', '[et_pb_text]ONLY ON C[/et_pb_text]'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling Q25: text parts are visible – raw HTML between blocks and the inner HTML of core blocks count
|
||||
* for the content, like attributes.
|
||||
*
|
||||
* @dataProvider visibleExtras
|
||||
*/
|
||||
public function testVisibleTextPartsCountForTheContent(string $extra, string $other): void
|
||||
{
|
||||
$plain = self::section();
|
||||
|
||||
self::assertNotSame(Fingerprint::content($plain), Fingerprint::content(self::sectionWith($extra)));
|
||||
self::assertNotSame(
|
||||
Fingerprint::content(self::sectionWith($extra)),
|
||||
Fingerprint::content(self::sectionWith($other))
|
||||
);
|
||||
self::assertNotSame(Fingerprint::canonical($plain), Fingerprint::canonical(self::sectionWith($extra)));
|
||||
}
|
||||
|
||||
public function testRawTextKeepsTheStructureAndWhitespaceNeverCounts(): void
|
||||
{
|
||||
$plain = self::section();
|
||||
|
||||
self::assertSame(Fingerprint::structure($plain), Fingerprint::structure(self::sectionWith('<p>x</p>')));
|
||||
self::assertSame(Fingerprint::content($plain), Fingerprint::content(self::sectionWith("\n \n")));
|
||||
self::assertSame(Fingerprint::canonical($plain), Fingerprint::canonical(self::sectionWith("\n\t")));
|
||||
}
|
||||
|
||||
/** Opaque blocks count with all their bytes (a global instance's snapshot included). */
|
||||
public function testOpaqueBlocksCountWithAllTheirBytes(): void
|
||||
{
|
||||
$instance = static fn (string $text): Block => LibraryContent::roots(LibraryContent::load(
|
||||
new Parser(),
|
||||
'<!-- wp:divi/placeholder --><!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
|
||||
. '<!-- wp:divi/global-layout {"globalModule":"5","blockName":"divi/text"} --><p>' . $text
|
||||
. '</p><!-- /wp:divi/global-layout -->'
|
||||
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->'
|
||||
))[0];
|
||||
|
||||
self::assertNotSame(Fingerprint::content($instance('a')), Fingerprint::content($instance('b')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,4 +212,33 @@ final class LibraryFindSimilarAbilityTest extends TestCase
|
||||
self::assertCount(5, $rest['matches']);
|
||||
self::assertArrayNotHasKey('note', $rest);
|
||||
}
|
||||
|
||||
/** Ruling Q25: raw HTML next to the blocks is visible – such a copy is only "structure", never "identical". */
|
||||
public function testExtraVisibleTextIsNotIdentical(): void
|
||||
{
|
||||
$root = LibraryContent::roots(LibraryContent::load(
|
||||
new Parser(),
|
||||
DiviFixtures::markup('stage3/library--library-abschnitt.html')
|
||||
))[0];
|
||||
$copy = Serializer::block($root);
|
||||
$at = strrpos($copy, '<!-- /wp:divi/column -->');
|
||||
$changed = substr($copy, 0, (int) $at) . '<p>ONLY ON 8</p>' . substr($copy, (int) $at);
|
||||
LibraryFixtures::given([
|
||||
LibraryFixtures::item(1055),
|
||||
new WP_Post(['ID' => 8, 'post_type' => 'page', 'post_status' => 'publish', 'post_title' => 'Über uns',
|
||||
'post_content' => '<!-- wp:divi/placeholder -->' . $changed . '<!-- /wp:divi/placeholder -->']),
|
||||
]);
|
||||
Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) [
|
||||
'edit_posts' => 'edit_posts',
|
||||
'edit_others_posts' => 'edit_others_posts',
|
||||
]]);
|
||||
Functions\when('get_posts')->justReturn([8]);
|
||||
|
||||
$result = self::ability()->execute(['library_id' => 1055, 'post_type' => 'page']);
|
||||
|
||||
self::assertSame(['8 s0 structure'], array_map(
|
||||
static fn (array $m): string => $m['post_id'] . ' ' . $m['id'] . ' ' . $m['match'],
|
||||
$result['matches']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,4 +490,78 @@ final class LibraryMakeGlobalAbilityTest extends TestCase
|
||||
}
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
/** @return array<string, array{string}> */
|
||||
public static function visibleExtras(): array
|
||||
{
|
||||
return [
|
||||
'core paragraph' => ['<!-- wp:paragraph --><p>ONLY ON 8</p><!-- /wp:paragraph -->'],
|
||||
'raw text part' => ['<p>ONLY ON 8</p>'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling Q25 (review of Task 15): a copy with extra visible text – a core paragraph or raw HTML between
|
||||
* blocks – is no identical occurrence; replacing it would swap visible content.
|
||||
*
|
||||
* @dataProvider visibleExtras
|
||||
*/
|
||||
public function testACopyWithExtraVisibleTextIsNoIdenticalOccurrence(string $extra): void
|
||||
{
|
||||
$copy = $this->givenCopiesOf1058();
|
||||
$at = strrpos($copy, '<!-- /wp:divi/column -->');
|
||||
$changed = substr($copy, 0, (int) $at) . $extra . substr($copy, (int) $at);
|
||||
LibraryFixtures::given([
|
||||
LibraryFixtures::item(1058),
|
||||
new WP_Post(['ID' => 8, 'post_type' => 'page', 'post_status' => 'publish', 'post_title' => 'Team',
|
||||
'post_content' => '<!-- wp:divi/placeholder -->' . $changed . '<!-- /wp:divi/placeholder -->']),
|
||||
get_post(9),
|
||||
]);
|
||||
|
||||
$result = $this->ability()->execute(self::targets(['s0'], ['s1'], true));
|
||||
|
||||
self::assertToolError($result, 'validation_failed');
|
||||
self::assertArrayHasKey('targets.0.ids.0', self::errorFields($result));
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
/** Scope (ruling Q25): only pages and posts – other Divi-enabled types are neither targets nor sources. */
|
||||
public function testOnlyPagesAndPostsAreTargetsAndSources(): void
|
||||
{
|
||||
$this->givenCopiesOf1058();
|
||||
$project = get_post(8);
|
||||
$project->post_type = 'project';
|
||||
LibraryFixtures::given([LibraryFixtures::item(1058), $project, get_post(9)]);
|
||||
|
||||
$target = $this->ability()->execute(self::targets(['s0'], ['s1'], true));
|
||||
self::assertToolError($target, 'validation_failed');
|
||||
self::assertArrayHasKey('targets.0.post_id', self::errorFields($target));
|
||||
|
||||
$source = $this->ability()->execute(
|
||||
['post_id' => 8, 'tree_rev' => Rev::of((string) $project->post_content), 'id' => 's0', 'title' => 'x']
|
||||
);
|
||||
self::assertToolError($source, 'validation_failed');
|
||||
self::assertArrayHasKey('post_id', self::errorFields($source));
|
||||
self::assertSame([], $this->saved);
|
||||
self::assertSame([], $this->specs);
|
||||
}
|
||||
|
||||
/** A rolled-back page N>1 keeps DiviSaver's note on the revision that stays. */
|
||||
public function testAFailureMidwayKeepsTheRevisionNote(): void
|
||||
{
|
||||
$this->givenCopiesOf1058();
|
||||
$kept = WriteFailed::rolledBack(
|
||||
'Die Divi-Seite konnte nicht gespeichert werden.',
|
||||
state: \LIT\MCP\Abilities\Errors::notSavedKeeping('Revision 812')
|
||||
);
|
||||
|
||||
try {
|
||||
$this->ability([], true, false, [null, $kept])->execute(self::targets(['s0'], ['s1'], true));
|
||||
self::fail('no WriteFailed');
|
||||
} catch (WriteFailed $e) {
|
||||
$message = (string) $e->toError()->get_error_message();
|
||||
self::assertStringContainsString('Revision 812 bleibt erhalten', $message);
|
||||
self::assertStringContainsString('Bereits ersetzt', $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user