Compare commits
2 Commits
f7fdf3930c
...
fix/browse
| Author | SHA1 | Date | |
|---|---|---|---|
| a2a6dc86fb | |||
| 693e09a6cf |
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
|||||||
name: static
|
name: static
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- run: cp .env.example .env
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -39,7 +40,7 @@ jobs:
|
|||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.composer/cache/files
|
path: ~/.composer/cache/files
|
||||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
@@ -55,7 +56,8 @@ jobs:
|
|||||||
name: unit
|
name: unit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- run: cp .env.example .env
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -63,7 +65,7 @@ jobs:
|
|||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.composer/cache/files
|
path: ~/.composer/cache/files
|
||||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
@@ -92,7 +94,8 @@ jobs:
|
|||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 10
|
--health-retries 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- run: cp .env.example .env
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -100,13 +103,13 @@ jobs:
|
|||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.composer/cache/files
|
path: ~/.composer/cache/files
|
||||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
restore-keys: composer-${{ runner.os }}-
|
restore-keys: composer-${{ runner.os }}-
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -136,7 +139,8 @@ jobs:
|
|||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 10
|
--health-retries 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- run: cp .env.example .env
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -144,13 +148,13 @@ jobs:
|
|||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.composer/cache/files
|
path: ~/.composer/cache/files
|
||||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
restore-keys: composer-${{ runner.os }}-
|
restore-keys: composer-${{ runner.os }}-
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -224,7 +228,7 @@ jobs:
|
|||||||
name: container
|
name: container
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Build production image
|
- name: Build production image
|
||||||
run: docker build -t amare-app:ci .
|
run: docker build -t amare-app:ci .
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,6 +18,7 @@
|
|||||||
/public/fonts-manifest.dev.json
|
/public/fonts-manifest.dev.json
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
|
/tests/Browser/Screenshots
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/storage/pail
|
/storage/pail
|
||||||
/vendor
|
/vendor
|
||||||
|
|||||||
16
tasks.md
16
tasks.md
@@ -61,3 +61,19 @@
|
|||||||
- `PageMeta::forErrorPage` landed on `main` during implementation; rebased and extended it for 419/429/503.
|
- `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.
|
- Contact form landed on `main`; submit-state browser coverage was restored.
|
||||||
- `event_type: null` violates NOT NULL column — test uses `''` instead (guard covers both).
|
- `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)
|
||||||
|
|||||||
@@ -41,13 +41,14 @@ $viewports = [
|
|||||||
foreach ($screens as $screen => $path) {
|
foreach ($screens as $screen => $path) {
|
||||||
foreach ($viewports as $viewport => [$width, $height]) {
|
foreach ($viewports as $viewport => [$width, $height]) {
|
||||||
it("matches {$screen} {$viewport} visual baseline", function () use ($path, $width, $height): void {
|
it("matches {$screen} {$viewport} visual baseline", function () use ($path, $width, $height): void {
|
||||||
|
$this->assertStableScreenshotMatches(
|
||||||
$this->visit($path, [
|
$this->visit($path, [
|
||||||
'reducedMotion' => 'reduce',
|
'reducedMotion' => 'reduce',
|
||||||
])
|
])
|
||||||
->withLocale('pt-BR')
|
->withLocale('pt-BR')
|
||||||
->withTimezone('America/Fortaleza')
|
->withTimezone('America/Fortaleza')
|
||||||
->resize($width, $height)
|
->resize($width, $height)
|
||||||
->assertScreenshotMatches();
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
tests/Support/MismatchScreenshotExporter.php
Normal file
40
tests/Support/MismatchScreenshotExporter.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
tests/Support/StableScreenshot.php
Normal file
68
tests/Support/StableScreenshot.php
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
use Tests\Support\StableScreenshot;
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
abstract class TestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
//
|
use StableScreenshot;
|
||||||
}
|
}
|
||||||
|
|||||||
47
tests/Unit/Support/MismatchScreenshotExporterTest.php
Normal file
47
tests/Unit/Support/MismatchScreenshotExporterTest.php
Normal 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([]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user