| 1 |
<?php |
| 2 |
/** |
| 3 |
* Standalone test runner for WProofreader_Content_Cleaner. |
| 4 |
* |
| 5 |
* Usage (inside a WP container): php tests/test-content-cleaner.php /path/to/wp-load.php |
| 6 |
* Exits non-zero on failure. |
| 7 |
*/ |
| 8 |
|
| 9 |
$wp_load = $argv[1] ?? '/var/www/web/wp/wp-load.php'; |
| 10 |
require $wp_load; |
| 11 |
|
| 12 |
if ( ! class_exists( 'WProofreader_Content_Cleaner' ) ) { |
| 13 |
require dirname( __DIR__ ) . '/includes/class-wproofreader-content-cleaner.php'; |
| 14 |
} |
| 15 |
|
| 16 |
$failures = 0; |
| 17 |
|
| 18 |
function check( string $name, bool $condition, string $detail = '' ) { |
| 19 |
global $failures; |
| 20 |
if ( $condition ) { |
| 21 |
echo "PASS {$name}\n"; |
| 22 |
} else { |
| 23 |
$failures++; |
| 24 |
echo "FAIL {$name}" . ( $detail ? " — {$detail}" : '' ) . "\n"; |
| 25 |
} |
| 26 |
} |
| 27 |
|
| 28 |
function clean( string $html ): string { |
| 29 |
return WProofreader_Content_Cleaner::clean_content( $html ); |
| 30 |
} |
| 31 |
|
| 32 |
function hook_path( string $html ): string { |
| 33 |
$data = WProofreader_Content_Cleaner::clean_post_data( |
| 34 |
array( 'post_content' => wp_slash( $html ), 'post_status' => 'private' ), |
| 35 |
array() |
| 36 |
); |
| 37 |
return wp_unslash( $data['post_content'] ); |
| 38 |
} |
| 39 |
|
| 40 |
// 1. Clean content without artifact markers is untouched (wsc- URLs included). |
| 41 |
$plain = '<p>Hello <a href="https://webspellchecker.com/wsc-proofreader/">link</a></p>'; |
| 42 |
check( 'clean content untouched', clean( $plain ) === $plain ); |
| 43 |
|
| 44 |
// 2. Simple artifact span is unwrapped. |
| 45 |
check( |
| 46 |
'simple span unwrapped', |
| 47 |
clean( '<p><span class="wsc-spelling-problem">word</span> rest</p>' ) === '<p>word rest</p>' |
| 48 |
); |
| 49 |
|
| 50 |
// 3. Multi-class artifact span with extra attributes. |
| 51 |
check( |
| 52 |
'multi-class span with attributes', |
| 53 |
clean( '<p><span data-wsc-id="9" class="wsc-spelling-problem wsc-problem-42" style="background:red">word</span></p>' ) === '<p>word</p>' |
| 54 |
); |
| 55 |
|
| 56 |
// 4. Nested artifact spans. |
| 57 |
check( |
| 58 |
'nested artifact spans', |
| 59 |
clean( '<p><span class="wsc-grammar-problem">a <span class="wsc-spelling-problem">b</span> c</span></p>' ) === '<p>a b c</p>' |
| 60 |
); |
| 61 |
|
| 62 |
// 5. Artifact span nested inside a regular span: regular span preserved byte-identical. |
| 63 |
check( |
| 64 |
'regular span preserved', |
| 65 |
clean( "<p><span class='keep' data-x='1'>a <span class=\"wsc-spelling-problem\">b</span></span></p>" ) === "<p><span class='keep' data-x='1'>a b</span></p>" |
| 66 |
); |
| 67 |
|
| 68 |
// 6. rangySelectionBoundary removal. |
| 69 |
check( |
| 70 |
'rangy boundary removed', |
| 71 |
clean( "<p>a<span class=\"rangySelectionBoundary\">\xE2\x80\x8B</span>b</p>" ) === "<p>a\xE2\x80\x8Bb</p>" |
| 72 |
); |
| 73 |
|
| 74 |
// 7. Unclosed artifact opener: opener dropped, rest intact. |
| 75 |
check( |
| 76 |
'unclosed artifact opener', |
| 77 |
clean( '<p><span class="wsc-spelling-problem">word</p>' ) === '<p>word</p>' |
| 78 |
); |
| 79 |
|
| 80 |
// 8. Quoting / entities / boolean attributes outside artifacts stay byte-identical. |
| 81 |
$tricky = '<figure class=\'"wp-block-table\' custom><a href=\'"https://x"\' rel=\'"nofollow"\'>x</a>' |
| 82 |
. '<span class="wsc-spelling-problem">w</span> & <br><svg viewBox="0 0 1 1"><rect/></svg></figure>'; |
| 83 |
$expected = '<figure class=\'"wp-block-table\' custom><a href=\'"https://x"\' rel=\'"nofollow"\'>x</a>' |
| 84 |
. 'w & <br><svg viewBox="0 0 1 1"><rect/></svg></figure>'; |
| 85 |
check( 'byte-identical outside splices', clean( $tricky ) === $expected ); |
| 86 |
|
| 87 |
// 9. Gutenberg block comments survive. |
| 88 |
$blocks = "<!-- wp:paragraph -->\n<p><span class=\"wsc-grammar-problem\">Текст українською</span> 🚀</p>\n<!-- /wp:paragraph -->"; |
| 89 |
check( |
| 90 |
'block comments and unicode survive', |
| 91 |
clean( $blocks ) === "<!-- wp:paragraph -->\n<p>Текст українською 🚀</p>\n<!-- /wp:paragraph -->" |
| 92 |
); |
| 93 |
|
| 94 |
// 10. Slashed hook path: regression for the 3.1.1 corruption bug. |
| 95 |
// Realistic Gutenberg fixture: quoted block attributes, existing " entities, |
| 96 |
// and a wsc- URL that used to false-positive the old substring guard. |
| 97 |
$baseline = '<!-- wp:heading {"level":2,"anchor":"h-custom"} -->' . "\n" |
| 98 |
. '<h2 class="wp-block-heading" id="h-custom">Say "hi"</h2>' . "\n" |
| 99 |
. '<!-- /wp:heading -->' . "\n" |
| 100 |
. '<!-- wp:image {"id":42,"sizeSlug":"large","linkDestination":"custom"} -->' . "\n" |
| 101 |
. '<figure class="wp-block-image size-large"><a href="https://webspellchecker.com/wsc-proofreader/">' |
| 102 |
. '<img src="https://x.test/a.png" alt="alt with "quotes""/></a></figure>' . "\n" |
| 103 |
. '<!-- /wp:image -->' . "\n" |
| 104 |
. '<!-- wp:paragraph -->' . "\n" |
| 105 |
. '<p>Plain <a href="https://webspellchecker.com/wsc-proofreader/" rel="noopener">link</a> text.</p>' . "\n" |
| 106 |
. '<!-- /wp:paragraph -->'; |
| 107 |
if ( file_exists( '/tmp/before_editor_save.html' ) ) { |
| 108 |
$real = file_get_contents( '/tmp/before_editor_save.html' ); |
| 109 |
check( 'slashed hook path lossless on real post baseline', hook_path( $real ) === $real ); |
| 110 |
} |
| 111 |
check( 'slashed hook path lossless on clean content', hook_path( $baseline ) === $baseline ); |
| 112 |
|
| 113 |
// 11. Slashed hook path with artifacts: cleaned, no " inflation. |
| 114 |
$dirty = str_replace( '<p>', '<p><span class="wsc-spelling-problem">x</span> ', $baseline ); |
| 115 |
$cleaned = hook_path( $dirty ); |
| 116 |
check( 'slashed hook path removes artifacts', strpos( $cleaned, 'wsc-spelling-problem' ) === false ); |
| 117 |
check( |
| 118 |
'slashed hook path adds no quot entities', |
| 119 |
substr_count( $cleaned, '"' ) === substr_count( $baseline, '"' ) |
| 120 |
); |
| 121 |
|
| 122 |
// 11b. Double invocation (Gutenberg REST save + meta-box-loader re-save) is idempotent. |
| 123 |
$pass1 = hook_path( $dirty ); |
| 124 |
$pass2 = hook_path( $pass1 ); |
| 125 |
check( 'double hook run idempotent', $pass2 === $pass1 ); |
| 126 |
check( 'double hook run adds no quot entities', substr_count( $pass2, '"' ) === substr_count( $baseline, '"' ) ); |
| 127 |
|
| 128 |
// 12. Autosave/revision statuses are no longer skipped. |
| 129 |
$autosave = WProofreader_Content_Cleaner::clean_post_data( |
| 130 |
array( 'post_content' => wp_slash( '<p><span class="wsc-spelling-problem">w</span></p>' ), 'post_status' => 'inherit' ), |
| 131 |
array() |
| 132 |
); |
| 133 |
check( 'inherit status cleaned', wp_unslash( $autosave['post_content'] ) === '<p>w</p>' ); |
| 134 |
|
| 135 |
// 13. Empty content untouched. |
| 136 |
$empty = WProofreader_Content_Cleaner::clean_post_data( array( 'post_content' => '' ), array() ); |
| 137 |
check( 'empty content untouched', '' === $empty['post_content'] ); |
| 138 |
|
| 139 |
// 14. Span with artifact class substring in another class name must NOT match. |
| 140 |
$lookalike = '<p><span class="wsc-spelling-problem-note">keep wrapper</span></p>'; |
| 141 |
check( 'class lookalike preserved', clean( $lookalike ) === $lookalike ); |
| 142 |
|
| 143 |
echo $failures ? "\n{$failures} FAILURE(S)\n" : "\nALL PASS\n"; |
| 144 |
exit( $failures ? 1 : 0 ); |
| 145 |
|