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
1,165 changes: 1,165 additions & 0 deletions docs/superpowers/plans/2026-10-06-bo-picker-snapshot.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions docs/superpowers/plans/2026-10-06-bo-picker-snapshot.md.tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"planPath": "docs/superpowers/plans/2026-10-06-bo-picker-snapshot.md",
"tasks": [
{
"id": 1,
"subject": "Task 1 : `PageSnapshot::captureCurrent()`",
"status": "completed",
"description": "Voir le plan, section « Task 1 : `PageSnapshot::captureCurrent()` ».\n\n```json:metadata\n{\"files\": [\"src/Visual/PageSnapshot.php\", \"tests/Unit/Visual/PageSnapshotTest.php\"], \"verifyCommand\": \"`php vendor/bin/phpunit tests/Unit/Visual/PageSnapshotTest.php` → OK (8 tests)\", \"acceptanceCriteria\": [\"`captureCurrent(object $page, int $timeoutMs = 15000): SnapshotResult` fait, dans l'ordre : stabilité, `SETTLE_ANIMATIONS`, statut, dimensions (hauteur plafonnée à `MAX_HEIGHT`), `scrollTo(0, 0)`, `ELEMENT_MAP`, capture JPEG qualité 80.\", \"Elle ne navigue pas et ne ferme pas le navigateur.\", \"`take()` crée le navigateur, navigue (même message d'erreur), appelle `captureCurrent($page, $timeoutMs)` et ferme le navigateur dans son `finally`.\", \"Les 6 tests existants de `PageSnapshotTest` passent sans modification de leurs assertions.\", \"`take()` et `captureCurrent()` donnent le même `SnapshotResult` sur la même page factice.\"]}\n```"
},
{
"id": 2,
"subject": "Task 2 : plafonds de navigation et de connexion, sans changer le run",
"status": "completed",
"description": "Voir le plan, section « Task 2 : plafonds de navigation et de connexion, sans changer le run ».\n\n```json:metadata\n{\"files\": [\"src/Pages/CommonPage.php\", \"src/Pages/BackOfficePage.php\", \"src/Pages/v9/BackOffice/Login/Page.php\", \"tests/Unit/Pages/BackOfficeGoToMenuTest.php\", \"tests/Unit/Pages/BackOfficeLoginOutcomeTest.php\"], \"verifyCommand\": \"`php vendor/bin/phpunit tests/Unit/Pages/BackOfficeGoToMenuTest.php tests/Unit/Pages/BackOfficeLoginOutcomeTest.php` → OK\", \"acceptanceCriteria\": [\"`public ?int $navigationTimeout = null;` sur `CommonPage`, transmis comme 2e argument de `waitForNavigation()` par `goToUrl()`, `BackOfficePage::goToPage()` et `BackOfficePage::goToMenu()`. `null` = défaut chrome-php (30 s), donc comportement actuel.\", \"`public int $loginOutcomeTimeout = 60000;` et `public ?bool $loginOutcomeSeen = null;` sur `v9\\\\BackOffice\\\\Login\\\\Page` (hérités par v7 et v8).\", \"`login()` (avec `$waitForNavigation`) appelle `waitForLoginOutcome($this->loginOutcomeTimeout)` et range le booléen renvoyé dans `$loginOutcomeSeen`.\", \"Signatures de `login()`, `waitForLoginOutcome()`, `goToPage()`, `goToMenu()` et `goToUrl()` inchangées.\"]}\n```",
"blockedBy": [
1
]
},
{
"id": 3,
"subject": "Task 3 : `openBackOfficeCheckpoint()`, `closeBackOfficeSession()` et `BackOfficeTimeoutException`",
"status": "completed",
"description": "Voir le plan, section « Task 3 : `openBackOfficeCheckpoint()`, `closeBackOfficeSession()` et `BackOfficeTimeoutException` ».\n\n```json:metadata\n{\"files\": [\"src/Exceptions/BackOfficeTimeoutException.php\", \"src/Tests/VisualTestsSuite.php\", \"tests/Unit/Visual/VisualTestsSuiteBackOfficeTest.php\"], \"verifyCommand\": \"`php vendor/bin/phpunit tests/Unit/Visual/VisualTestsSuiteBackOfficeTest.php` → OK\", \"acceptanceCriteria\": [\"`openBackOfficeCheckpoint(array $checkpoint, int $loginTimeoutMs = 25000, int $menuTimeoutMs = 15000): string` :\", \"`closeBackOfficeSession(int $timeoutMs = 5000): void` appelle `logout()` de la page Login une seule fois si une session est ouverte, sous le plafond donné (rétabli ensuite), et ne lève jamais.\", \"Les tests existants de `VisualTestsSuiteBackOfficeTest` passent sans changement de leurs assertions (journaux identiques).\"]}\n```",
"blockedBy": [
2
]
},
{
"id": 4,
"subject": "Task 4 : livraison (coordinateur)",
"status": "in_progress",
"description": "Voir le plan, section « Task 4 : livraison (coordinateur) ».\n\n```json:metadata\n{\"files\": [], \"verifyCommand\": \"\", \"acceptanceCriteria\": [\"Relecture finale (`rtk proxy git diff dev...HEAD > /private/tmp/claude-501/-Users-jonathan-Documents-GitHub-PrestaFlow-app/2393cd17-28d4-44c5-96f2-fa24f981a8c8/scratchpad/lib-branch.diff`). Points à vérifier :\", \"Suite complète verte en 8.4 et en 8.1 (sorties redirigées).\", \"Push de `feat/bo-picker-snapshot`, PR vers `dev` (titre : « Capture du sélecteur visuel en back-office »), description terminée par la ligne d'attribution prévue. CI verte.\", \"Sur demande seulement, dans cet ordre :\"]}\n```",
"blockedBy": [
3
]
}
],
"lastUpdated": "2026-10-06T13:51:03+00:00"
}
102 changes: 102 additions & 0 deletions docs/superpowers/specs/2026-10-06-bo-picker-snapshot-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Capture du sélecteur visuel en back-office (lib) : design

Date : 2026-10-06
Statut : validé, à implémenter

Partie lib du sélecteur visuel BO de l'app PrestaFlow. La spec complète, côté
app, est `docs/superpowers/specs/2026-10-06-visual-bo-picker-design.md` du dépôt
PrestaFlow/app.

## Contexte

L'app veut ouvrir, pour son sélecteur visuel, la même page BO qu'un run :
connexion employé, tableau de bord, entrée du menu, ou page de connexion
déconnectée. Elle veut ensuite capturer cette page (carte des éléments et image)
avec `PageSnapshot`.

Aujourd'hui, `PageSnapshot::take()` ne sait que naviguer vers une URL. La
connexion et le menu n'existent qu'à l'intérieur du run de `VisualTestsSuite`
(`ensureBackOfficeLogin`, `goToMenu`).

## Design

### `PageSnapshot::captureCurrent()`

- Nouvelle méthode publique :
`captureCurrent(object $page, int $timeoutMs = 15000): SnapshotResult` (typé `object` pour les doubles de test ; en pratique une `\HeadlessChromium\Page`).
- Elle contient tout ce que `take()` fait après la navigation :
- attente de stabilité ;
- `PageScripts::SETTLE_ANIMATIONS` ;
- statut HTTP ;
- dimensions du document (hauteur plafonnée) ;
- `PageScripts::ELEMENT_MAP` ;
- capture pleine page en JPEG.
- `take()` crée son navigateur, navigue, puis appelle `captureCurrent()`. Son
comportement et son résultat ne changent pas : les tests existants le prouvent.

### `VisualTestsSuite::openBackOfficeCheckpoint()` et `closeBackOfficeSession()`

- `openBackOfficeCheckpoint(array $checkpoint, int $loginTimeoutMs = 25000, int $menuTimeoutMs = 15000): string`, publique.
Elle renvoie le chemin et le contrôleur de la page ouverte, sans jeton (`''` si
illisible).
- Elle ouvre, dans le navigateur courant de la suite (`TestsSuite::getPage()`),
la page d'un checkpoint BO, **avec le même code que le run** :
- le checkpoint est normalisé par `normalize()` ;
- `auth: false` : racine du BO, et l'erreur « session déjà ouverte » si le
formulaire de connexion est absent ;
- sinon : `ensureBackOfficeLogin()`, puis `goToPage('index')` et, si `menu` est
défini, `goToMenu($menu)`.
- Budget global : échéance = début + `$loginTimeoutMs` + `$menuTimeoutMs` (40 s
par défaut). Chaque étape plafonnée reçoit min(son plafond, reste avant
l'échéance). Une étape n'est pas lancée s'il reste moins de 1 s.
- Navigations (page de connexion, tableau de bord, menu) : min(`$menuTimeoutMs`, reste).
- Envoi du formulaire : seulement s'il reste au moins 16 s. `login()` attend
d'abord un rechargement de 10 s fixes (`waitForPageReload()`), et
`isLoggedIn()` jusqu'à 5 s après l'issue. L'issue de la connexion
(`waitForLoginOutcome`) reçoit max(1 s, min(`$loginTimeoutMs`, reste) − 10 s
− 5 s). Si `$loginTimeoutMs` + `$menuTimeoutMs` < 17 s, le formulaire n'est
jamais envoyé.
- Pire cas : échéance + lectures JS et remplissage du formulaire (≤ 5 s
chacun), non plafonnés par ce budget. La capture et la déconnexion sont
hors de ce budget.
- Un dépassement lève une exception de délai dédiée
(`BackOfficeTimeoutException`), au message lisible (« … dans le délai
imparti (N s) »).
- Les messages relayés ne portent ni jeton ni identifiants d'URL (`user:pass@`).
La cause d'origine reste en `previous`.
- Les erreurs sont celles du run (identifiants refusés avec le message du
formulaire, page inattendue avec son chemin, entrée de menu introuvable). Elles
sont relevées comme `\RuntimeException`, avec le message déjà formulé par le
run.
- Elle exige `area === 'bo'`, sinon `\LogicException`.
- `closeBackOfficeSession(int $timeoutMs = 5000): void`, publique : déconnexion par le lien
`#header_logout` de la page courante (`BackOffice\Login\Page::logout()`) si une
session est ouverte. Elle ne lève jamais : un échec de déconnexion ne doit pas
faire échouer la capture.
- Aucune autre méthode du run ne change de comportement. `ensureBackOfficeLogin` et
`goToMenu` restent la seule implémentation de la connexion et du menu ;
`ensureBackOfficeLogin` gagne seulement la cause en `previous` et un point
d’accroche privé avant l’envoi du formulaire, inactif pendant un run.

### Tests de la lib

- `captureCurrent` : `take()` donne le même `SnapshotResult` qu'avant. On le vérifie
avec les doubles de `PageSnapshotTest`, en appelant `captureCurrent` sur une page
factice.
- `openBackOfficeCheckpoint`, avec des pages factices (comme
`VisualTestsSuiteBackOfficeTest`) :
- ordre connexion → tableau de bord → menu ;
- checkpoint `auth: false` (pas de connexion) ;
- identifiants refusés ;
- menu introuvable ;
- délai dépassé ;
- refus hors BO.
- `closeBackOfficeSession` : appelle `logout()` quand une session est ouverte, ne
lève jamais.

### Livraison de la lib

- PR vers `dev`, merge en commit de merge.
- Release `dev` → `main` en commit de merge.
- Bump dans l'app : seul `composer.lock` change. Voir le flux de release de la lib.

12 changes: 12 additions & 0 deletions src/Exceptions/BackOfficeTimeoutException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace PrestaFlow\Library\Exceptions;

/**
* Le back-office n'a pas répondu dans le plafond donné (issue de la connexion,
* ou chargement d'une page) : levée par VisualTestsSuite::openBackOfficeCheckpoint().
* Message lisible, destiné à l'utilisateur.
*/
class BackOfficeTimeoutException extends TimeoutException
{
}
4 changes: 2 additions & 2 deletions src/Pages/BackOfficePage.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function goToPage($page = null, $params = null)
// FrontOffice (ou au 1er appel) — évite un close+createPage inutile
// pour deux navigations BO consécutives.
TestsSuite::recreatePageIfContextChanged('BO');
$this->getPage()->navigate($url)->waitForNavigation(\HeadlessChromium\Page::DOM_CONTENT_LOADED);
$this->getPage()->navigate($url)->waitForNavigation(\HeadlessChromium\Page::DOM_CONTENT_LOADED, $this->navigationTimeout);
}

public function getPageURL($page, $params = null): string
Expand Down Expand Up @@ -204,7 +204,7 @@ public function goToMenu(string $selectors): string
if (!is_string($href) || $href === '' || (str_contains($href, '#') && explode('#', $href, 2)[0] === $currentBase)) {
continue;
}
$page->navigate($href)->waitForNavigation(\HeadlessChromium\Page::DOM_CONTENT_LOADED);
$page->navigate($href)->waitForNavigation(\HeadlessChromium\Page::DOM_CONTENT_LOADED, $this->navigationTimeout);

return $href;
}
Expand Down
12 changes: 11 additions & 1 deletion src/Pages/CommonPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ class CommonPage
public string $url = '';
public string $pageTitle = '';

/**
* Plafond (ms) des navigations de goToUrl() et, en back-office, de
* goToPage() / goToMenu() ; null = défaut de chrome-php (30 s). Posé puis
* rétabli par VisualTestsSuite::openBackOfficeCheckpoint() pour le
* sélecteur visuel de l'app, qui doit rester sous les 60 s de nginx.
* FrontOfficePage::goToUrl() redéfinit goToUrl() et ne l'applique pas
* (goToUrlInPlace(), qui passe par CommonPage::goToUrl(), l'applique).
*/
public ?int $navigationTimeout = null;

protected $patchVersion = null;

public function __construct(string $locale, string $patchVersion, array $globals, array $customs = [])
Expand Down Expand Up @@ -909,7 +919,7 @@ public function goToUrl(string $url)
// DOM_CONTENT_LOADED (au lieu du LOAD par défaut) : on n'attend pas les
// assets tardifs (images, tracking, iframes). Les getters ont chacun leur
// waitUntilContainsElement, donc c'est safe pour la majorité des cas.
$this->getPage()->navigate($url)->waitForNavigation(DomPage::DOM_CONTENT_LOADED);
$this->getPage()->navigate($url)->waitForNavigation(DomPage::DOM_CONTENT_LOADED, $this->navigationTimeout);
}

/**
Expand Down
18 changes: 17 additions & 1 deletion src/Pages/v9/BackOffice/Login/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ class Page extends BasePage
{
public string $pageTitle = 'PrestaShop';

/**
* Plafond (ms) de l'attente de l'issue de la connexion dans login().
* 60 s pour un run (premier tableau de bord lent en CI, voir
* waitForLoginOutcome()) ; le sélecteur visuel de l'app le baisse.
*/
public int $loginOutcomeTimeout = 60000;

/**
* Issue de la dernière connexion de login() : true = vue (session ouverte
* ou alerte d'erreur), false = plafond atteint sans issue, null = pas encore
* attendue. isLoggedIn() reste le constat de la session.
*/
public ?bool $loginOutcomeSeen = null;

public function defineSelectors()
{
return [
Expand Down Expand Up @@ -40,6 +54,8 @@ public function defineMessages()
*/
public function login($email = null, $password = null, $waitForNavigation = true)
{
// Issue de CETTE connexion seulement : sans attente, elle reste inconnue.
$this->loginOutcomeSeen = null;
if ($email === null) {
$email = $this->getGlobal('BO_EMAIL');
}
Expand All @@ -54,7 +70,7 @@ public function login($email = null, $password = null, $waitForNavigation = true
if ($waitForNavigation) {
$this->click($this->getSelector('submitLoginButton'));
$this->waitForPageReload();
$this->waitForLoginOutcome();
$this->loginOutcomeSeen = $this->waitForLoginOutcome($this->loginOutcomeTimeout);
} else {
$this->click($this->getSelector('submitLoginButton'));
}
Expand Down
Loading
Loading