Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion php-transformer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"scripts": {
"parity": "@test:parity",
"static-parity": "php tools/static-parity/run.php",
"live-wp-parity": "php tools/live-wp-parity/run.php",
"corpus-diagnostics": "php tools/corpus-diagnostics/run.php",
"test": [
"@test:canonical",
Expand All @@ -55,7 +56,8 @@
"php tests/unit/subtree-classifier.php",
"php tests/unit/custom-block-generator.php",
"php tests/unit/css-value-splitter.php",
"php tests/unit/corpus-detectors.php"
"php tests/unit/corpus-detectors.php",
"php tests/unit/live-wp-parity-runner.php"
],
"test:parity": "php tests/parity/run.php",
"test:migration:examples": "php tests/migration/examples.php",
Expand Down
47 changes: 45 additions & 2 deletions php-transformer/src/VisualParity/StaticStyleParityRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,51 @@ public function compareSourceToTransform(string $sourceHtml, string $authorCss =
$result = $this->transformer->transform($sourceHtml, array())->toArray();
$candidateHtml = self::candidateHtmlFromSerializedBlocks((string) ($result['serialized_blocks'] ?? ''));

$sourceProbes = $this->probe->extract($sourceHtml, $authorCss);
$candidateProbes = $this->probe->extract($candidateHtml, $authorCss);
// The render-free proxy carries the SAME author CSS to both sides so the
// only variable is the transformed DOM.
return $this->compareSourceToCandidate($sourceHtml, $candidateHtml, $authorCss, $authorCss);
}

/**
* Compare a source document against an EXTERNALLY produced candidate document.
*
* Unlike {@see compareSourceToTransform}, which builds the candidate render-free
* from serialized blocks, this entry accepts a candidate HTML string produced by
* a real WordPress render (e.g. the DOM HTML wp-codebox fetches after SSI import
* + activate). It runs the IDENTICAL deterministic probe + comparator so the
* report contract, score, and per-property diff are exactly the same shape as the
* render-free gate — only the candidate's provenance differs.
*
* This exercises WordPress's own block rendering + global-styles layer (the layer
* the render-free proxy cannot see) while staying fully deterministic: no browser,
* no rasterization, no network, no screenshots. The candidate's effective styling
* is resolved statically from whatever CSS the rendered DOM already carries
* (WP global-styles / block-supports / layout `<style>` blocks) plus any explicit
* $candidateCss the caller inlined; nothing is fetched.
*
* The source and candidate take SEPARATE CSS inputs on purpose: the source side
* carries the fixture's own author CSS, while the live-WP candidate must be judged
* solely on the styling WordPress actually emitted — feeding the source's author
* CSS to the candidate would mask exactly the rendering/global-styles regressions
* this variant exists to catch.
*
* @param string $sourceHtml Source HTML document.
* @param string $candidateHtml Candidate HTML document (e.g. live-WP rendered DOM).
* @param string $sourceCss Author CSS merged into the source-side cascade.
* @param string $candidateCss Extra CSS merged into the candidate-side cascade.
* Defaults to empty: a self-contained rendered DOM
* already carries its own <style> blocks.
* @return array<string, mixed> VisualParityReportContract report (same contract
* as {@see compareSourceToTransform}).
*/
public function compareSourceToCandidate(
string $sourceHtml,
string $candidateHtml,
string $sourceCss = '',
string $candidateCss = ''
): array {
$sourceProbes = $this->probe->extract($sourceHtml, $sourceCss);
$candidateProbes = $this->probe->extract($candidateHtml, $candidateCss);

return $this->comparator->compare($sourceProbes, $candidateProbes);
}
Expand Down
157 changes: 157 additions & 0 deletions php-transformer/tests/unit/live-wp-parity-runner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);

/**
* Unit tests for the live-WP external-candidate runner entry
* ({@see StaticStyleParityRunner::compareSourceToCandidate}).
*
* Plain-PHP test script in the style of tests/unit/css-value-splitter.php — no
* PHPUnit. The live-WP variant must:
* 1. Run the EXISTING deterministic comparator against an external candidate and
* produce a byte-identical report across repeated runs (determinism).
* 2. Reduce to the render-free proxy when fed the proxy's own candidate HTML and
* the same CSS on both sides (wiring proof — same comparator, same contract).
* 3. Name the exact diverged CSS property when WP's render emits different
* effective styling than the source (the bug class this variant exists for).
* 4. Judge the candidate solely on its own rendered styling: the source's author
* CSS must NOT leak onto the candidate side.
*/

require dirname(__DIR__, 2) . '/vendor/autoload.php';

use Automattic\BlocksEngine\PhpTransformer\VisualParity\StaticStyleParityRunner;

$failures = 0;
$passes = 0;

$assert = static function (bool $condition, string $message, string $detail = '') use (&$failures, &$passes): void {
if ( $condition ) {
++$passes;
return;
}

++$failures;
fwrite(STDERR, 'FAIL: ' . $message . ('' !== $detail ? ' - ' . $detail : '') . PHP_EOL);
};

$runner = new StaticStyleParityRunner();

$sourceHtml = <<<'HTML'
<!doctype html>
<html><head><style>
.hero { color: #112233; font-size: 24px; text-align: center; }
.cta { background-color: #ff0000; border-radius: 8px; }
</style></head>
<body>
<div class="hero">Welcome aboard</div>
<a class="cta">Get started</a>
</body></html>
HTML;

// A faithful "live-WP render": same content, same effective styling, but expressed
// the way WordPress would emit it (block wrapper classes + an inline global-styles
// <style> block carrying the same declarations). The DOM differs; the effective
// style on the matched content does not.
$candidateFaithful = <<<'HTML'
<!doctype html>
<html><head><style id="global-styles-inline-css">
.hero { color: #112233; font-size: 24px; text-align: center; }
.cta { background-color: #ff0000; border-radius: 8px; }
</style></head>
<body>
<div class="wp-block-group">
<div class="hero">Welcome aboard</div>
<a class="cta">Get started</a>
</div>
</body></html>
HTML;

// A regressed "live-WP render": WP's rendering layer dropped the CTA background
// color and shifted the hero color — exactly the global-styles/block-supports
// regression class the render-free proxy cannot see.
$candidateRegressed = <<<'HTML'
<!doctype html>
<html><head><style id="global-styles-inline-css">
.hero { color: #999999; font-size: 24px; text-align: center; }
.cta { border-radius: 8px; }
</style></head>
<body>
<div class="wp-block-group">
<div class="hero">Welcome aboard</div>
<a class="cta">Get started</a>
</div>
</body></html>
HTML;

// ---------------------------------------------------------------------------
// 1. Determinism: same inputs -> byte-identical report across two runs.
// ---------------------------------------------------------------------------
$run1 = $runner->compareSourceToCandidate($sourceHtml, $candidateRegressed);
$run2 = $runner->compareSourceToCandidate($sourceHtml, $candidateRegressed);
$json1 = json_encode($run1);
$json2 = json_encode($run2);
$assert($json1 === $json2, '1: external-candidate report is byte-identical across runs');

// ---------------------------------------------------------------------------
// 2. Wiring proof: feeding the render-free proxy's own candidate HTML through the
// external entry (same CSS both sides) reproduces compareSourceToTransform.
// ---------------------------------------------------------------------------
$proxyCss = '.hero { color: #112233; font-size: 24px; } .cta { background-color: #ff0000; }';
$proxyReport = $runner->compareSourceToTransform($sourceHtml, $proxyCss);
$transformResult = (new \Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer())
->transform($sourceHtml, array())->toArray();
$proxyCandidateHtml = StaticStyleParityRunner::candidateHtmlFromSerializedBlocks(
(string) ($transformResult['serialized_blocks'] ?? '')
);
$viaExternal = $runner->compareSourceToCandidate($sourceHtml, $proxyCandidateHtml, $proxyCss, $proxyCss);
$assert(
json_encode($proxyReport) === json_encode($viaExternal),
'2: external entry reproduces the render-free proxy when fed the proxy candidate + same CSS'
);

// ---------------------------------------------------------------------------
// 3. Faithful live render scores perfectly (parity == 1.0, no findings).
// ---------------------------------------------------------------------------
$faithful = $runner->compareSourceToCandidate($sourceHtml, $candidateFaithful);
$faithfulScore = (float) ($faithful['parity']['score'] ?? 0.0);
$faithfulFindings = (int) ($faithful['summary']['finding_total'] ?? -1);
$assert($faithfulScore >= 0.999, '3: faithful live render reaches full parity', sprintf('score=%.4f', $faithfulScore));
$assert(0 === $faithfulFindings, '3b: faithful live render yields no findings', sprintf('findings=%d', $faithfulFindings));

// ---------------------------------------------------------------------------
// 4. Regressed live render is caught and names the exact diverged properties.
// ---------------------------------------------------------------------------
$regressed = $runner->compareSourceToCandidate($sourceHtml, $candidateRegressed);
$regressedScore = (float) ($regressed['parity']['score'] ?? 1.0);
$assert($regressedScore < $faithfulScore, '4: regressed live render scores below faithful render', sprintf('regressed=%.4f faithful=%.4f', $regressedScore, $faithfulScore));

$divergedProperties = array();
foreach ( (array) ($regressed['matches'] ?? array()) as $match ) {
foreach ( (array) ($match['style_deltas'] ?? array()) as $delta ) {
$divergedProperties[(string) ($delta['property'] ?? '')] = true;
}
}
$assert(isset($divergedProperties['background-color']), '4b: dropped CTA background-color is reported as a per-property diff');
$assert(isset($divergedProperties['color']), '4c: shifted hero color is reported as a per-property diff');

// ---------------------------------------------------------------------------
// 5. Source author CSS must NOT leak onto the candidate side. If the source's CSS
// were applied to the candidate, the regressed candidate (whose own <style>
// dropped the background) would falsely score as a match.
// ---------------------------------------------------------------------------
$sourceOnlyCss = '.cta { background-color: #ff0000; }';
$leakCheck = $runner->compareSourceToCandidate($sourceHtml, $candidateRegressed, $sourceOnlyCss, '');
$leakDiverged = array();
foreach ( (array) ($leakCheck['matches'] ?? array()) as $match ) {
foreach ( (array) ($match['style_deltas'] ?? array()) as $delta ) {
$leakDiverged[(string) ($delta['property'] ?? '')] = true;
}
}
$assert(isset($leakDiverged['background-color']), '5: source author CSS does not leak onto the candidate side');

if ( $failures > 0 ) {
fwrite(STDERR, "live-wp-parity runner unit tests: {$failures} failed, {$passes} passed\n");
exit(1);
}

fwrite(STDOUT, "live-wp-parity runner unit tests: {$passes} passed\n");
Loading
Loading