Compare commits

...

2 Commits

Author SHA1 Message Date
a2a6dc86fb docs: track browser CI gate task status in tasks.md 2026-08-06 08:47:57 -03:00
693e09a6cf fix: stabilize and diagnose browser CI gate
- Replace networkidle screenshot wait (5s client timeout, flaky with
  long-lived connections) with readyState + fonts-loaded wait and fixed
  settle in a StableScreenshot helper.
- Export standalone diff/expected/actual PNGs on visual mismatch so CI
  artifacts are directly viewable (vendor only writes an HTML diff view).
- Create .env in CI test jobs; without it Laravel's env bootstrap emits a
  file_get_contents warning on every test.
- Bump checkout/cache actions to v5 (Node 20 deprecation).
- Gitignore tests/Browser/Screenshots.
2026-08-06 08:41:18 -03:00
8 changed files with 197 additions and 19 deletions

View File

@@ -31,7 +31,8 @@ jobs:
name: static
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- run: cp .env.example .env
- uses: shivammathur/setup-php@v2
with:
@@ -39,7 +40,7 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
coverage: none
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
@@ -55,7 +56,8 @@ jobs:
name: unit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- run: cp .env.example .env
- uses: shivammathur/setup-php@v2
with:
@@ -63,7 +65,7 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
coverage: none
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
@@ -92,7 +94,8 @@ jobs:
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- run: cp .env.example .env
- uses: shivammathur/setup-php@v2
with:
@@ -100,13 +103,13 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
coverage: none
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
@@ -136,7 +139,8 @@ jobs:
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- run: cp .env.example .env
- uses: shivammathur/setup-php@v2
with:
@@ -144,13 +148,13 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
coverage: none
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
@@ -224,7 +228,7 @@ jobs:
name: container
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build production image
run: docker build -t amare-app:ci .

1
.gitignore vendored
View File

@@ -18,6 +18,7 @@
/public/fonts-manifest.dev.json
/public/hot
/public/storage
/tests/Browser/Screenshots
/storage/*.key
/storage/pail
/vendor

View File

@@ -61,3 +61,19 @@
- `PageMeta::forErrorPage` landed on `main` during implementation; rebased and extended it for 419/429/503.
- Contact form landed on `main`; submit-state browser coverage was restored.
- `event_type: null` violates NOT NULL column — test uses `''` instead (guard covers both).
# Browser CI Gate — estabilidade e diagnosticabilidade
## BG — Causa raiz
- [x] Diagnosticar 3 runs: mismatch visual (8 testes), acessibilidade (1), timeout networkidle 5s + overflow real
- [x] Reproduzir local: `waitForLoadState('networkidle')` fragil (conexões longas), default 5s
- [x] Reproduzir warning `file_get_contents(.env)` em TODO teste (CI sem .env)
## BG — Fixes
- [x] Helper `StableScreenshot` (tests/Support/): drop networkidle → readyState + fonts + settle
- [x] Exportar PNGs standalone (diff/expected/actual) no mismatch — visíveis nos artifacts
- [x] CI: `cp .env.example .env` nos jobs de teste; checkout/cache v5
- [x] Gitignore tests/Browser/Screenshots
- [x] Unit test MismatchScreenshotExporter; pint/phpstan/unit/feature verdes
- [ ] Push + PR + CI browser verde
- [ ] Refresh baselines de CI se necessário (timing mudou ligeiramente)

View File

@@ -41,13 +41,14 @@ $viewports = [
foreach ($screens as $screen => $path) {
foreach ($viewports as $viewport => [$width, $height]) {
it("matches {$screen} {$viewport} visual baseline", function () use ($path, $width, $height): void {
$this->visit($path, [
'reducedMotion' => 'reduce',
])
->withLocale('pt-BR')
->withTimezone('America/Fortaleza')
->resize($width, $height)
->assertScreenshotMatches();
$this->assertStableScreenshotMatches(
$this->visit($path, [
'reducedMotion' => 'reduce',
])
->withLocale('pt-BR')
->withTimezone('America/Fortaleza')
->resize($width, $height)
);
});
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Tests\Support;
final class MismatchScreenshotExporter
{
/**
* Extracts the diff, expected, and actual images embedded in an ImageDiffView
* HTML file and writes them as standalone PNG files so they can be inspected
* directly in CI artifacts.
*
* @return list<string> paths of the written PNG files
*/
public static function export(string $htmlPath, string $targetDir): array
{
$html = (string) file_get_contents($htmlPath);
if (preg_match_all('/src="data:image\/png;base64,([^"]+)"/', $html, $matches) === false || ! isset($matches[1][2])) {
return [];
}
$base = pathinfo($htmlPath, PATHINFO_FILENAME);
$labels = ['diff', 'expected', 'actual'];
$written = [];
foreach (array_slice($matches[1], 0, 3) as $index => $base64) {
$path = $targetDir.'/'.$base.'-'.($labels[$index] ?? 'image-'.$index).'.png';
if (file_put_contents($path, (string) base64_decode($base64, true)) === false) {
continue;
}
$written[] = $path;
}
return $written;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Tests\Support;
use Pest\Browser\Api\AwaitableWebpage;
use Pest\Browser\Exceptions\BrowserExpectationFailedException;
use Pest\Browser\Support\Screenshot;
use Pest\TestSuite;
use PHPUnit\Framework\ExpectationFailedException;
trait StableScreenshot
{
/**
* Asserts the page matches its visual baseline without relying on the
* networkidle load state, which is inherently flaky (long-lived connections
* like fonts, polling, or keep-alive can keep it from ever firing and cause
* spurious timeouts at the 5s client default).
*
* On mismatch the vendor only writes an HTML diff view with base64-embedded
* images, so this helper additionally exports standalone diff/expected/actual
* PNG files that are directly viewable in CI artifacts.
*/
public function assertStableScreenshotMatches(AwaitableWebpage $awaitable): void
{
$page = $awaitable->page();
$page->addStyleTag('* {
transition: none !important;
animation: none !important;
font-family: Arial, sans-serif !important;
body {
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
}');
$page->waitForFunction(
'document.readyState === "complete"'
.' && document.fonts.status === "loaded"'
);
usleep(300_000);
try {
$page->expectScreenshot(true, false);
} catch (ExpectationFailedException $exception) {
$this->exportMismatchScreenshots();
throw BrowserExpectationFailedException::from($page, $exception);
}
}
private function exportMismatchScreenshots(): void
{
[$snapshotName] = TestSuite::getInstance()->snapshots->get();
$base = pathinfo($snapshotName, PATHINFO_FILENAME);
$htmlPath = Screenshot::dir().'/ImageDiffView/'.$base.'.html';
if (! is_file($htmlPath)) {
return;
}
MismatchScreenshotExporter::export($htmlPath, Screenshot::dir());
}
}

View File

@@ -3,8 +3,9 @@
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Support\StableScreenshot;
abstract class TestCase extends BaseTestCase
{
//
use StableScreenshot;
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
use Tests\Support\MismatchScreenshotExporter;
it('exports diff, expected, and actual PNGs from an ImageDiffView html', function (): void {
$targetDir = sys_get_temp_dir().'/mismatch-exporter-'.uniqid();
mkdir($targetDir, 0755, true);
$expected = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgAAACAAFWoS02AAAAAElFTkSuQmCC';
$actual = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgAAACAAFWoS02AAAAAElFTkSuQmCC';
$diff = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgAAACAAFWoS02AAAAAElFTkSuQmCC';
$html = <<<HTML
<!DOCTYPE html>
<html><body>
<img alt="Diff" src="data:image/png;base64,{$diff}"/>
<img slot="image-1" alt="Expected" src="data:image/png;base64,{$expected}"/>
<img slot="image-2" alt="Actual" src="data:image/png;base64,{$actual}"/>
</body></html>
HTML;
$htmlPath = $targetDir.'/it_matches_home_desktop.html';
file_put_contents($htmlPath, $html);
$written = MismatchScreenshotExporter::export($htmlPath, $targetDir);
expect($written)->toHaveCount(3);
expect($written)->toContain($targetDir.'/it_matches_home_desktop-diff.png');
expect($written)->toContain($targetDir.'/it_matches_home_desktop-expected.png');
expect($written)->toContain($targetDir.'/it_matches_home_desktop-actual.png');
expect(base64_decode($diff, true))->toBe((string) file_get_contents($targetDir.'/it_matches_home_desktop-diff.png'));
expect(base64_decode($expected, true))->toBe((string) file_get_contents($targetDir.'/it_matches_home_desktop-expected.png'));
expect(base64_decode($actual, true))->toBe((string) file_get_contents($targetDir.'/it_matches_home_desktop-actual.png'));
});
it('returns an empty list when the html has no embedded images', function (): void {
$targetDir = sys_get_temp_dir().'/mismatch-exporter-'.uniqid();
mkdir($targetDir, 0755, true);
$htmlPath = $targetDir.'/plain.html';
file_put_contents($htmlPath, '<html><body>no images</body></html>');
expect(MismatchScreenshotExporter::export($htmlPath, $targetDir))->toBe([]);
});