175 lines
6.1 KiB
PHP
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 · {$engines} · generated {$generated}</p>
|
|
</header>
|
|
{$body}
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
}
|
|
}
|