Files
ppeasy_vg12/tests/Visual/Gallery.php
T
2026-10-09 08:21:06 +02:00

175 lines
6.1 KiB
PHP

<?php
namespace Tests\Visual;
/**
* Collects the gallery captures and builds the page you browse them on.
*
* The plugin writes every screenshot into tests/Browser/Screenshots and wipes
* that directory at the start of each run, so a three browser pass would erase
* the previous engine's images. Captures are therefore moved into
* tests/Browser/Gallery/{browser} once the run's tests are done, which is after
* the last capture but before the next run's cleanup.
*/
class Gallery
{
public const PREFIX = 'gallery__';
/**
* Viewports every page is captured at, as [device method, label].
*
* @var array<string, array{string, string}>
*/
public const VIEWPORTS = [
'desktop' => ['desktop', 'desktop'],
'tablet' => ['iPadPro', 'tablet'],
'mobile' => ['iPhone15', 'mobile'],
];
/**
* The engine this run is driving.
*
* Set by the test:visual composer script. Playwright::defaultBrowserType()
* would report the same thing but is marked @internal.
*/
public static function browser(): string
{
return getenv('VANGUARD_BROWSER') ?: 'chrome';
}
public static function dir(): string
{
return base_path('tests/Browser/Gallery');
}
/**
* The filename a capture is written under, before it is collected.
*/
public static function filename(string $page, string $viewport): string
{
return self::PREFIX.$page.'__'.$viewport;
}
/**
* Move this run's captures into the gallery, then rebuild the index.
*/
public static function collect(): void
{
$target = self::dir().'/'.self::browser();
if (! is_dir($target)) {
mkdir($target, 0755, true);
}
foreach (glob(base_path('tests/Browser/Screenshots/'.self::PREFIX.'*.png')) ?: [] as $capture) {
rename($capture, $target.'/'.str_replace(self::PREFIX, '', basename($capture)));
}
self::buildIndex();
}
/**
* Write the browsable index over everything captured so far.
*
* Reads the directory rather than tracking state, so the index stays
* complete when only one browser is re-run.
*/
public static function buildIndex(): void
{
$browsers = array_map('basename', array_filter(glob(self::dir().'/*') ?: [], 'is_dir'));
sort($browsers);
$pages = [];
foreach ($browsers as $browser) {
foreach (glob(self::dir().'/'.$browser.'/*.png') ?: [] as $image) {
[$page, $viewport] = array_pad(explode('__', basename($image, '.png'), 2), 2, 'desktop');
$pages[$page][$viewport][$browser] = $browser.'/'.basename($image);
}
}
ksort($pages);
file_put_contents(self::dir().'/index.html', self::render($pages, $browsers));
}
/**
* @param array<string, array<string, array<string, string>>> $pages
* @param array<int, string> $browsers
*/
private static function render(array $pages, array $browsers): string
{
$generated = date('Y-m-d H:i');
$engines = implode(', ', $browsers);
$count = array_sum(array_map(fn (array $viewports): int => array_sum(array_map('count', $viewports)), $pages));
$body = '';
foreach ($pages as $page => $viewports) {
$title = htmlspecialchars(str_replace('-', ' ', $page));
$body .= "<section><h2>{$title}</h2>";
foreach (array_keys(self::VIEWPORTS) as $viewport) {
if (! isset($viewports[$viewport])) {
continue;
}
$body .= '<h3>'.htmlspecialchars($viewport).'</h3><div class="row">';
foreach ($browsers as $browser) {
$src = $viewports[$viewport][$browser] ?? null;
$body .= '<figure>'.($src === null
? '<div class="missing">not captured</div>'
: '<a href="'.htmlspecialchars($src).'" target="_blank" rel="noopener">'
.'<img loading="lazy" src="'.htmlspecialchars($src).'" alt=""></a>')
.'<figcaption>'.htmlspecialchars($browser).'</figcaption></figure>';
}
$body .= '</div>';
}
$body .= '</section>';
}
return <<<HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vanguard UI gallery</title>
<style>
:root { color-scheme: light dark; }
body { margin: 0; padding: 2rem; font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
header { margin-bottom: 2.5rem; }
h1 { margin: 0 0 .25rem; font-size: 1.5rem; }
.meta { color: #64748b; font-size: .875rem; }
section { margin-bottom: 3rem; }
h2 { font-size: 1.125rem; text-transform: capitalize; border-bottom: 1px solid #e2e8f0; padding-bottom: .5rem; }
h3 { font-size: .75rem; text-transform: uppercase; letter-spacing: .08em; color: #64748b; margin: 1.5rem 0 .5rem; }
.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; }
figure { margin: 0; }
img { width: 100%; border: 1px solid #e2e8f0; border-radius: 6px; display: block; background: #fff; }
.missing { border: 1px dashed #cbd5e1; border-radius: 6px; padding: 2rem; text-align: center; color: #94a3b8; font-size: .8125rem; }
figcaption { margin-top: .375rem; font-size: .8125rem; color: #64748b; }
@media (prefers-color-scheme: dark) {
body { background: #0f172a; color: #e2e8f0; }
h2 { border-color: #1e293b; }
img { border-color: #1e293b; }
}
</style>
</head>
<body>
<header>
<h1>Vanguard UI gallery</h1>
<p class="meta">{$count} captures &middot; {$engines} &middot; generated {$generated}</p>
</header>
{$body}
</body>
</html>
HTML;
}
}