Some checks failed
Deploy Application / deploy (push) Has been cancelled
191 lines
5.4 KiB
PHP
191 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Framework\LiveComponents\TestHarness;
|
|
|
|
/**
|
|
* Snapshot Testing Utilities for LiveComponents
|
|
*
|
|
* Provides snapshot comparison for component render output with whitespace normalization.
|
|
*
|
|
* Usage:
|
|
* ```php
|
|
* $snapshot = ComponentSnapshotTest::createSnapshot('counter-initial', $html);
|
|
* ComponentSnapshotTest::assertMatchesSnapshot($html, 'counter-initial');
|
|
* ```
|
|
*/
|
|
final readonly class ComponentSnapshotTest
|
|
{
|
|
private const SNAPSHOT_DIR = __DIR__ . '/../../../../tests/snapshots/livecomponents';
|
|
|
|
/**
|
|
* Normalize HTML for snapshot comparison
|
|
*
|
|
* Removes:
|
|
* - Extra whitespace
|
|
* - Line breaks
|
|
* - Multiple spaces
|
|
* - CSRF tokens (dynamic)
|
|
* - Timestamps (dynamic)
|
|
*/
|
|
public static function normalizeHtml(string $html): string
|
|
{
|
|
// Remove CSRF tokens (they're dynamic)
|
|
$html = preg_replace('/data-csrf-token="[^"]*"/', 'data-csrf-token="[CSRF_TOKEN]"', $html);
|
|
|
|
// Remove timestamps (they're dynamic)
|
|
$html = preg_replace('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', '[TIMESTAMP]', $html);
|
|
|
|
// Remove component IDs with random parts (keep structure)
|
|
$html = preg_replace('/data-component-id="([^:]+):[^"]*"/', 'data-component-id="$1:[ID]"', $html);
|
|
|
|
// Normalize whitespace
|
|
$html = preg_replace('/\s+/', ' ', $html);
|
|
$html = trim($html);
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Create or update snapshot
|
|
*
|
|
* @param string $snapshotName Snapshot name (without extension)
|
|
* @param string $html HTML to snapshot
|
|
* @return string Path to snapshot file
|
|
*/
|
|
public static function createSnapshot(string $snapshotName, string $html): string
|
|
{
|
|
self::ensureSnapshotDir();
|
|
|
|
$normalized = self::normalizeHtml($html);
|
|
$filePath = self::getSnapshotPath($snapshotName);
|
|
|
|
file_put_contents($filePath, $normalized);
|
|
|
|
return $filePath;
|
|
}
|
|
|
|
/**
|
|
* Assert HTML matches snapshot
|
|
*
|
|
* @param string $html HTML to compare
|
|
* @param string $snapshotName Snapshot name
|
|
* @param bool $updateSnapshot If true, update snapshot instead of asserting
|
|
* @throws \PHPUnit\Framework\AssertionFailedError If snapshot doesn't match
|
|
*/
|
|
public static function assertMatchesSnapshot(
|
|
string $html,
|
|
string $snapshotName,
|
|
bool $updateSnapshot = false
|
|
): void {
|
|
$filePath = self::getSnapshotPath($snapshotName);
|
|
$normalized = self::normalizeHtml($html);
|
|
|
|
if ($updateSnapshot || !file_exists($filePath)) {
|
|
self::createSnapshot($snapshotName, $html);
|
|
return;
|
|
}
|
|
|
|
$expected = file_get_contents($filePath);
|
|
|
|
if ($normalized !== $expected) {
|
|
$diff = self::generateDiff($expected, $normalized);
|
|
|
|
throw new \PHPUnit\Framework\AssertionFailedError(
|
|
"Snapshot '{$snapshotName}' does not match.\n\n" .
|
|
"Expected:\n{$expected}\n\n" .
|
|
"Actual:\n{$normalized}\n\n" .
|
|
"Diff:\n{$diff}\n\n" .
|
|
"To update snapshot, set updateSnapshot=true or delete: {$filePath}"
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get snapshot file path
|
|
*/
|
|
private static function getSnapshotPath(string $snapshotName): string
|
|
{
|
|
return self::SNAPSHOT_DIR . '/' . $snapshotName . '.snapshot';
|
|
}
|
|
|
|
/**
|
|
* Ensure snapshot directory exists
|
|
*/
|
|
private static function ensureSnapshotDir(): void
|
|
{
|
|
if (!is_dir(self::SNAPSHOT_DIR)) {
|
|
mkdir(self::SNAPSHOT_DIR, 0755, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate diff between expected and actual
|
|
*/
|
|
private static function generateDiff(string $expected, string $actual): string
|
|
{
|
|
$expectedLines = explode("\n", $expected);
|
|
$actualLines = explode("\n", $actual);
|
|
|
|
$diff = [];
|
|
$maxLines = max(count($expectedLines), count($actualLines));
|
|
|
|
for ($i = 0; $i < $maxLines; $i++) {
|
|
$expectedLine = $expectedLines[$i] ?? null;
|
|
$actualLine = $actualLines[$i] ?? null;
|
|
|
|
if ($expectedLine === $actualLine) {
|
|
$diff[] = " {$i}: {$expectedLine}";
|
|
} else {
|
|
if ($expectedLine !== null) {
|
|
$diff[] = "- {$i}: {$expectedLine}";
|
|
}
|
|
if ($actualLine !== null) {
|
|
$diff[] = "+ {$i}: {$actualLine}";
|
|
}
|
|
}
|
|
}
|
|
|
|
return implode("\n", $diff);
|
|
}
|
|
|
|
/**
|
|
* List all snapshots
|
|
*
|
|
* @return array<string> Snapshot names
|
|
*/
|
|
public static function listSnapshots(): array
|
|
{
|
|
self::ensureSnapshotDir();
|
|
|
|
$snapshots = [];
|
|
$files = glob(self::SNAPSHOT_DIR . '/*.snapshot');
|
|
|
|
foreach ($files as $file) {
|
|
$snapshots[] = basename($file, '.snapshot');
|
|
}
|
|
|
|
return $snapshots;
|
|
}
|
|
|
|
/**
|
|
* Delete snapshot
|
|
*/
|
|
public static function deleteSnapshot(string $snapshotName): bool
|
|
{
|
|
$filePath = self::getSnapshotPath($snapshotName);
|
|
|
|
if (file_exists($filePath)) {
|
|
return unlink($filePath);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|