Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions src/Pages/CommonPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,65 @@ public function scrollToAnchor(string $selector, int $settleMs = 400): void
}
}

private function applyVisualMasks(array $masks): void
{
$masks = array_values(array_filter(array_map('trim', $masks)));
if ($masks === []) {
return;
}
// Une règle CSS par sélecteur : un sélecteur invalide ne doit pas faire
// échouer le bloc entier et désactiver le masquage des autres.
$css = json_encode(
implode("\n", array_map(fn ($m) => $m.' { visibility: hidden !important; }', $masks)),
JSON_THROW_ON_ERROR
);
$this->getPage()->evaluate(
"(function(){var s=document.createElement('style');s.id='pf-visual-masks';s.textContent={$css};document.head.appendChild(s);})()"
)->getReturnValue();
}

private function removeVisualMasks(array $masks): void
{
if (array_filter(array_map('trim', $masks)) === []) {
return;
}
try {
$this->getPage()->evaluate(
"(function(){var s=document.getElementById('pf-visual-masks');if(s){s.remove();}})()"
)->getReturnValue();
} catch (\Throwable $e) {
// best-effort : ne doit jamais masquer l'exception d'origine de la capture
}
}

/** Scrolle juste sous l'élément (ex. header) : le viewport commence au contenu utile. */
public function scrollBelow(string $selector, int $settleMs = 400): void
{
try {
$sel = json_encode($selector, JSON_THROW_ON_ERROR);
$this->getPage()->evaluate(
"(function(){window.scrollTo(0, 0);var e=document.querySelector({$sel});if(!e){return;}var r=e.getBoundingClientRect();window.scrollTo(0, r.bottom + window.scrollY);})()"
)->getReturnValue();
usleep($settleMs * 1000);
} catch (\Throwable $e) {
// best-effort : l'écart de capture tranchera si le scroll a échoué
}
}

/** Attend le chargement complet + polices + images (y compris lazy sous le pli), sans lever si le délai expire. */
public function waitForStable(int $timeout = 10000): void
{
try {
$this->waitForJsCondition(
"document.readyState === 'complete' && (!document.fonts || document.fonts.status === 'loaded') "
. "&& Array.from(document.images).every(function(i){return i.complete || (i.loading === 'lazy' && i.getBoundingClientRect().top > window.innerHeight);})",
$timeout
);
} catch (\Throwable $e) {
// best-effort : ne doit jamais lever, même en cas d'échec du polling
}
}

/**
* Best-effort : dimensions du viewport courant via le navigateur. `null`
* pour un des deux (ou les deux) si l'info n'est pas disponible (page
Expand Down Expand Up @@ -474,7 +533,7 @@ private function getViewportSize(): array
* VisualTag::resolve()). Un tag libre est utilisé tel quel — il doit
* matcher `^[a-z0-9._-]+$`, sinon exception explicite.
*/
public function visualCheckpoint(string $name, ?string $selector = null, float $threshold = 0.98, bool $fullPage = true, string $tag = 'auto'): void
public function visualCheckpoint(string $name, ?string $selector = null, float $threshold = 0.98, bool $fullPage = true, string $tag = 'auto', array $masks = []): void
{
$rawMajorVersion = $this->getMajorVersion();
$majorVersion = (is_string($rawMajorVersion) || is_int($rawMajorVersion))
Expand All @@ -498,21 +557,26 @@ public function visualCheckpoint(string $name, ?string $selector = null, float $

$page = $this->getPage();

if ($selector !== null) {
$node = $page->dom()->querySelector($selector);
if ($node === null) {
throw new \RuntimeException("visualCheckpoint : sélecteur introuvable « {$selector} »");
$this->applyVisualMasks($masks);
try {
if ($selector !== null) {
$node = $page->dom()->querySelector($selector);
if ($node === null) {
throw new \RuntimeException("visualCheckpoint : sélecteur introuvable « {$selector} »");
}
$page->screenshotElement($node)->saveToFile($actualPath);
} elseif ($fullPage) {
$page->screenshot([
'captureBeyondViewport' => true,
'clip' => $page->getFullPageClip(),
'format' => 'png',
])->saveToFile($actualPath);
} else {
// Viewport seul : hauteur fixe (fenêtre), indépendante du total de la page.
$page->screenshot(['format' => 'png'])->saveToFile($actualPath);
}
$page->screenshotElement($node)->saveToFile($actualPath);
} elseif ($fullPage) {
$page->screenshot([
'captureBeyondViewport' => true,
'clip' => $page->getFullPageClip(),
'format' => 'png',
])->saveToFile($actualPath);
} else {
// Viewport seul : hauteur fixe (fenêtre), indépendante du total de la page.
$page->screenshot(['format' => 'png'])->saveToFile($actualPath);
} finally {
$this->removeVisualMasks($masks);
}

if (!is_file($refPath)) {
Expand Down
90 changes: 82 additions & 8 deletions src/Tests/TestsSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,79 @@ private static function buildVisualBlock(int $startIndex): array
*/
protected static ?string $browserFilesScope = null;

/** Overrides posés par l'app (matrice visuelle) avant la création du navigateur. */
protected static ?array $browserOptionOverrides = null;

/**
* Ne s'applique qu'à la PROCHAINE création de navigateur (dans getBrowser(),
* quand aucun socket vivant n'est réutilisé) : un navigateur déjà lancé garde
* sa taille/UA d'origine tant qu'on ne l'a pas fermé. Pour appliquer un nouvel
* override à un navigateur déjà en cours, appeler resetBrowser() juste après.
*/
public static function useBrowserOptions(?int $width, ?int $height, ?string $userAgent): void
{
self::$browserOptionOverrides = ($width === null && $height === null && $userAgent === null)
? null
: ['width' => $width, 'height' => $height, 'userAgent' => $userAgent];
}

/** @return array{windowSize: array{0:int,1:int}, userAgent: string} */
public static function browserOptions(): array
{
$o = self::$browserOptionOverrides ?? [];

$width = (int) (($o['width'] ?? null) ?: Env::get('PRESTAFLOW_WINDOW_SIZE_WIDTH'));
if ($width <= 0) {
$width = 1920;
}

$height = (int) (($o['height'] ?? null) ?: Env::get('PRESTAFLOW_WINDOW_SIZE_HEIGHT'));
if ($height <= 0) {
$height = 1080;
}

return [
'windowSize' => [$width, $height],
'userAgent' => (string) (($o['userAgent'] ?? null) ?: Env::get('PRESTAFLOW_USER_AGENT', 'PrestaFlow')),
];
}

/**
* Ferme le navigateur partagé et oublie son socket : le prochain getBrowser()
* en relancera un neuf (nécessaire quand la taille de fenêtre change).
*
* Si ce process n'a pas de navigateur en cache (self::$browserInstance) mais
* qu'un fichier socket existe, on tente de s'y connecter pour le fermer quand
* même : sinon un navigateur keepAlive lancé par un process précédent (ou
* après un crash) resterait orphelin en arrière-plan.
*
* Hypothèse : un seul process possède le navigateur partagé à la fois (le
* fichier socket est global, pas process-local). Des runs concurrents sur la
* même machine/storage peuvent donc se fermer mutuellement leur navigateur.
*/
public static function resetBrowser(): void
{
$socketFile = self::getFilePath('.browser');

try {
if (self::$browserInstance !== null) {
self::$browserInstance->close();
} elseif (file_exists($socketFile)) {
$socket = trim((string) file_get_contents($socketFile));
if ($socket !== '') {
BrowserFactory::connectToBrowser($socket)->close();
}
}
} catch (\Throwable) {
// navigateur déjà mort / injoignable : rien à faire
}

self::$browserInstance = null;
self::$browserInstanceSocket = null;
@unlink($socketFile);
@unlink(self::getFilePath('.browser-options'));
}

protected $draft = false;
protected $groups = 'all';

Expand Down Expand Up @@ -473,15 +546,16 @@ public static function getBrowser(bool $headless = true, bool $force = true)
return null;
}

// Dimensions de la fenêtre : PRESTAFLOW_WINDOW_SIZE_WIDTH/HEIGHT en
// env (utile pour émuler mobile/tablet/desktop). Défaut FHD 1920×1080.
$winWidth = (int) (Env::get('PRESTAFLOW_WINDOW_SIZE_WIDTH') ?: 1920);
$winHeight = (int) (Env::get('PRESTAFLOW_WINDOW_SIZE_HEIGHT') ?: 1080);
// Dimensions de la fenêtre et user agent : overrides posés par
// useBrowserOptions() sinon PRESTAFLOW_WINDOW_SIZE_WIDTH/HEIGHT /
// PRESTAFLOW_USER_AGENT en env (utile pour émuler mobile/tablet/desktop).
// Défaut FHD 1920×1080.
$opts = self::browserOptions();

$options = [
'userAgent' => Env::get('PRESTAFLOW_USER_AGENT', 'PrestaFlow'),
'userAgent' => $opts['userAgent'],
'keepAlive' => true,
'windowSize' => [$winWidth, $winHeight],
'windowSize' => $opts['windowSize'],
'headless' => (bool) $headless,
'ignoreCertificateErrors' => true,
];
Expand Down Expand Up @@ -969,8 +1043,8 @@ public function loadGlobals()
'VERBOSE' => (bool) Env::get('PRESTAFLOW_VERBOSE', true),
'BROWSER' => [
'HEADLESS' => (bool) Env::get('PRESTAFLOW_HEADLESS', true),
'WINDOW_SIZE_HEIGHT' => Env::get('PRESTAFLOW_WINDOW_SIZE_HEIGHT', 1920),
'WINDOW_SIZE_WIDTH' => Env::get('PRESTAFLOW_WINDOW_SIZE_WIDTH', 1000),
'WINDOW_SIZE_HEIGHT' => Env::get('PRESTAFLOW_WINDOW_SIZE_HEIGHT', 1080),
'WINDOW_SIZE_WIDTH' => Env::get('PRESTAFLOW_WINDOW_SIZE_WIDTH', 1920),
'USER_AGENT' => Env::get('PRESTAFLOW_USER_AGENT', 'PrestaFlow'),
],
];
Expand Down
Loading
Loading