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
952 changes: 952 additions & 0 deletions docs/superpowers/plans/2026-10-02-version-per-instance.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"planPath": "docs/superpowers/plans/2026-10-02-version-per-instance.md",
"tasks": [
{
"id": 1,
"subject": "Task 1: les pages prennent leur version à la construction",
"status": "completed",
"description": "**Goal:** initVersion() en tête des constructeurs CommonPage/BackOfficePage/FrontOfficePage ; InvalidVersionException importée dans Traits\\Version.\n\n```json:metadata\n{\"files\": [\"src/Traits/Version.php\", \"src/Pages/CommonPage.php\", \"src/Pages/BackOfficePage.php\", \"src/Pages/FrontOfficePage.php\", \"tests/Unit/Pages/PageVersionInitTest.php\"], \"verifyCommand\": \"php vendor/bin/phpunit tests/Unit/Pages/PageVersionInitTest.php\", \"acceptanceCriteria\": [\"patchVersion 1.7.8.11 -> majeure 1.7\", \"PS_VERSION des globals si patchVersion vide\", \"sans version -> InvalidVersionException\", \"8.1.10 -> InvalidVersionException, pas classe introuvable\", \"suite complète verte en 8.4\"]}\n```"
},
{
"id": 2,
"subject": "Task 2: la version devient une propriété d'instance",
"status": "completed",
"blockedBy": [
1
],
"description": "**Goal:** suppression de public static $versions, plus de repli sur '8', tests adaptés.\n\n```json:metadata\n{\"files\": [\"src/Traits/Version.php\", \"tests/Unit/Pages/PageVersionIsolationTest.php\", \"tests/Unit/Traits/VersionOverrideTest.php\", \"tests/Unit/Resolvers/TranslationsCatalogTest.php\", \"tests/Unit/Pages/BackOfficeGoToMenuTest.php\"], \"verifyCommand\": \"\\\"$HOME/Library/Application Support/Herd/bin/php81\\\" vendor/bin/phpunit tests/Unit/Pages/PageVersionIsolationTest.php\", \"acceptanceCriteria\": [\"deux pages BO gardent leur majeure\", \"BO 8.1 puis FO 9.0 isolées en PHP 8.1\", \"aucun ::$versions\", \"getMajorVersion sans version lève\", \"setVersions ne garde que 3 clés\", \"suite verte 8.4 et 8.1\"]}\n```"
},
{
"id": 3,
"subject": "Task 3: transmission explicite (scénario, import de page)",
"status": "completed",
"blockedBy": [
2
],
"description": "**Goal:** Scenario reprend le tableau de versions de la suite et analyse PATCH_VERSION ; importPage ne recopie plus les versions.\n\n```json:metadata\n{\"files\": [\"src/Scenarios/Scenario.php\", \"src/Traits/ImportPage.php\", \"tests/Unit/Scenarios/ScenarioVersionTest.php\", \"tests/Unit/Tests/SuiteVersionWiringTest.php\"], \"verifyCommand\": \"php vendor/bin/phpunit tests/Unit/Scenarios/ScenarioVersionTest.php tests/Unit/Tests/SuiteVersionWiringTest.php\", \"acceptanceCriteria\": [\"scénario sur suite 1.7.8.11 -> 1.7\", \"PATCH_VERSION analysé en tableau\", \"scénario isolé de la suite\", \"deux suites importent v7 puis v9\"]}\n```"
},
{
"id": 4,
"subject": "Task 4: la capture visuelle lit la version de sa page",
"status": "completed",
"blockedBy": [
2
],
"description": "**Goal:** retrait du contournement globals d'abord dans visualCheckpoint(), test Tag remplacé.\n\n```json:metadata\n{\"files\": [\"src/Pages/CommonPage.php\", \"tests/Unit/Visual/VisualCheckpointTagTest.php\"], \"verifyCommand\": \"php vendor/bin/phpunit tests/Unit/Visual/\", \"acceptanceCriteria\": [\"tag issu de la version de la page\", \"1.7 -> auto-v1.7\", \"plus de commentaire cache statique\"]}\n```"
},
{
"id": 5,
"subject": "Task 5: vérification sur PHP 8.1 à 8.4",
"status": "completed",
"blockedBy": [
3,
4
],
"description": "**Goal:** suite verte sous PHP 8.1/8.2/8.3/8.4 et en ordre aléatoire ; corrections de tests factices seulement.\n\n```json:metadata\n{\"files\": [], \"verifyCommand\": \"php vendor/bin/phpunit (x 4 binaires)\", \"acceptanceCriteria\": [\"vert 8.1-8.4\", \"ordre aléatoire 3 graines vert en 8.1\", \"aucun ::$versions\"]}\n```"
}
],
"lastUpdated": "2026-10-02T15:08:41+02:00"
}
134 changes: 134 additions & 0 deletions docs/superpowers/specs/2026-10-02-version-per-instance-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Version de PrestaShop portée par chaque objet : design

Date : 2026-10-02
Statut : implémenté (branche fix/version-per-instance)

## Problème

La version de PrestaShop (majeure, mineure, patch) vit dans un attribut
**statique** du trait `Traits\Version` (`public static $versions`).

- **Partage selon la version de PHP.** Un statique déclaré dans un trait
appartient à chaque classe qui fait `use` du trait.
- `CommonPage` l'obtient via `Translations` ; `BackOfficePage` et les pages
v7/v8/v9 partagent donc le statique de `CommonPage`.
- `FrontOfficePage` refait `use Translations` : avant PHP 8.3 il partage le
statique de `CommonPage`, à partir de 8.3 il a le sien.
- La lib déclare `php ^8.1` : le comportement diffère entre la CI (8.1 à 8.5)
et un poste en 8.4.
- **Argument ignoré.** Le constructeur d'une page reçoit `patchVersion` mais
ne l'utilise jamais. `BackOfficePage` et `FrontOfficePage` calculent leurs
sélecteurs et messages (`getSelectors()`, `getMessages()` →
`getPageName()` → `getMajorVersion(namespace: true)`) **avant**
`parent::__construct`, avec la version en cache, sinon `PS_VERSION` des
globals, sinon `'8'`.
- **Repli silencieux mis en cache.** Ce `'8'` est mémorisé dans le statique et
l'emporte ensuite sur la vraie version des pages suivantes. Constaté le
2026-10-02 : un test construisant une page sans version faisait lire des
sélecteurs v8 à une page v9 suivante, en PHP 8.1 et 8.2 seulement.
- **Bugs voisins.**
- `exctractVersions()` lève `InvalidVersionException` sans l'importer :
on obtient « classe introuvable » au lieu du message prévu.
- `Scenario` fait `setVersions($globals['PATCH_VERSION'] ?? …)` : une chaîne
peut remplacer le tableau des versions.

## Décisions

| Sujet | Décision |
|---|---|
| Où vit la version | Sur chaque objet (page, suite, scénario). Plus de statique. |
| Page sans version | `InvalidVersionException` explicite, plus de repli sur `'8'`. |
| Statique `$versions` | Supprimé. |
| Formats acceptés | Inchangés (le contrat avec l'app, qui refuse `8.1.10`, ne bouge pas). |
| API publique | Noms et signatures des getters/setters conservés (l'app les liste par leur nom). |

## 1. Trait `Version`

- `public static $versions` est remplacé par une propriété d'instance
`protected array $versions = ['patchVersion' => null, 'minorVersion' => null, 'majorVersion' => null]`.
Toutes les lectures et écritures passent par `$this->versions`.
- `getVersions()` / `setVersions(array $versions)` : `setVersions` est typé
`array` et ne garde que les trois clés connues.
- `getMajorVersion()` :
- renvoie la majeure de l'objet si elle est connue ;
- sinon, si `globals['PS_VERSION']` est présent, l'analyse
(`exctractVersions`) et renvoie la majeure ;
- sinon lève `InvalidVersionException` (« version PrestaShop inconnue »).
- Le repli sur `'8'` disparaît.
- `exctractVersions()` importe `PrestaFlow\Library\Exceptions\InvalidVersionException`.
Formats acceptés inchangés.
- `resolveVersion()` (suites) garde son ordre de priorité et son défaut
`'8.1.0'` : c'est le seul endroit avec un défaut.

## 2. Pages

- Nouvelle méthode `CommonPage::initVersion(string $patchVersion, array $globals): void` :
- `$patchVersion` non vide → `exctractVersions($patchVersion)` ;
- sinon `globals['PS_VERSION']` non vide → `exctractVersions(...)` ;
- sinon `InvalidVersionException`.
- Appelée **en premier** dans `CommonPage::__construct`, `BackOfficePage::__construct`
et `FrontOfficePage::__construct`, avant `getSelectors()` / `getMessages()`.
- `ImportPage::importPage()` continue de passer `patchVersion` et les globals ;
la recopie des versions après construction est supprimée : la page a pris
sa version dans son constructeur, et `initTranslations()` la recalcule déjà
à partir du même patch.

## 3. Scénarios

- `Scenario` reprend `$testSuite->getVersions()` (un tableau).
- Si `globals['PATCH_VERSION']` est présent, il est analysé par
`exctractVersions()` au lieu d'être passé tel quel à `setVersions()`.

## 4. Nettoyage

- `BackOfficeGoToMenuTest` : suppression de la sauvegarde / restauration de
`CommonPage::$versions` (setUp / tearDown).
- `CommonPage::visualCheckpoint()` : la majeure est lue par
`getMajorVersion()`, sans le contournement « globals d'abord ».
- Tests qui lisent ou écrivent le statique (`VersionOverrideTest`,
`TranslationsCatalogTest`, `BackOfficeGoToMenuTest`) : adaptés aux
accesseurs d'instance.
- Pages factices de tests construites sans version (ex. `makePage` des
tests `VisualCheckpoint*`) : elles reçoivent un `PS_VERSION` ou un
`patchVersion`.

## Tests

- Une page `BackOfficePage` 8.1 puis une page `FrontOfficePage` 9.0
construites l'une après l'autre : chacune garde sa majeure et ses
sélecteurs (et l'inverse).
- Une page construite avec `patchVersion: '1.7.8.11'` et sans `PS_VERSION`
a la majeure `1.7`.
- Une page sans aucune version lève `InvalidVersionException`, avec son
message (preuve que la classe est bien importée).
- `Scenario` avec `PATCH_VERSION` garde un tableau de versions cohérent.
- `resolveVersion()` des suites : comportement inchangé (tests existants).
- Suite unitaire complète verte sous **PHP 8.1, 8.2, 8.3 et 8.4**.
- CI de la PR : 13 checks verts (PHPUnit, Smoke, Visual).

## Livraison

- PR lib vers `dev`, puis release vers `main`.
- Puis mise à jour de la lib dans l'app (`composer.lock` seul) ; tests de
l'app verts.

## Hors périmètre

- Élargir les formats de version acceptés (ex. `8.1.10`).
- Revoir `resolveVersion()` et son défaut `'8.1.0'`.

## Notes d'implémentation

Écarts avec ce qui précède, décidés pendant l'implémentation :

- **Suites sans globals** : `TestsSuite::__construct(loadGlobals: false)` appelle
`resolveVersion()`. Sans cela, `before()` → `isVersionSupported()` dépendait
du repli sur `'8'`.
- **Analyseur unique** : `Version::parseVersions()` (statique) découpe une version ;
`exctractVersions()` et `importPage()` s'en servent. Un échec ne modifie plus
l'état de l'objet.
- **`importPage()`** : le namespace `vX` et la version de la page viennent du même
patch (`globals['PATCH_VERSION']`, sinon celui de l'appelant). Une classe de page
introuvable lève `InvalidVersionException`.
- **`initVersion()`** est `protected` : l'éditeur de l'app liste les méthodes
publiques des pages comme actions.
1 change: 1 addition & 0 deletions src/Pages/BackOfficePage.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class BackOfficePage extends CommonPage

public function __construct(string $locale, string $patchVersion, array $globals, array $customs = [])
{
$this->initVersion(patchVersion: $patchVersion, globals: $globals);
$this->globals = $globals;
$this->customs = array_merge($this->customs, $customs);
$this->initLocale(locale: $locale);
Expand Down
27 changes: 22 additions & 5 deletions src/Pages/CommonPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use HeadlessChromium\Exception\OperationTimedOut;
use HeadlessChromium\Exception\TargetDestroyed;
use HeadlessChromium\Page as DomPage;
use PrestaFlow\Library\Exceptions\InvalidVersionException;
use PrestaFlow\Library\Exceptions\TimeoutException;
use PrestaFlow\Library\Expects\Expect;
use PrestaFlow\Library\Resolvers\Translations;
Expand Down Expand Up @@ -46,6 +47,7 @@ class CommonPage

public function __construct(string $locale, string $patchVersion, array $globals, array $customs = [])
{
$this->initVersion(patchVersion: $patchVersion, globals: $globals);
$this->globals = $globals;
$this->customs = array_merge($this->customs, $customs);
$this->patchVersion = $patchVersion;
Expand All @@ -61,6 +63,24 @@ public function __construct(string $locale, string $patchVersion, array $globals
return $this;
}

/**
* Version PrestaShop de cette page : son patchVersion, sinon le
* PS_VERSION des globals. Appelée en tête des constructeurs, avant que
* getSelectors()/getMessages() ne choisissent les fichiers par majeure.
*/
protected function initVersion(string $patchVersion, array $globals): void
{
$version = $patchVersion !== '' ? $patchVersion : ($globals['PS_VERSION'] ?? null);

if (!is_string($version) || $version === '') {
throw new InvalidVersionException(
static::class . ' : version PrestaShop inconnue (ni patchVersion ni PS_VERSION dans les globals).'
);
}

$this->exctractVersions($version);
}

public function setCustoms(string $type, array $data = [])
{
if (isset($this->customs[$type]) && is_array($this->customs[$type])) {
Expand Down Expand Up @@ -697,13 +717,10 @@ public function visualCheckpoint(string $name, ?string $selector = null, ?float
$legacyRatio = $threshold !== null && $maxDiffPixels === null;
$budget = $legacyRatio ? null : max(0, $maxDiffPixels ?? self::DEFAULT_MAX_DIFF_PIXELS);

// globals PS_VERSION d'abord (vérité de la suite courante ; le cache statique
// de Version peut venir d'une suite précédente du worker), puis le cache.
// '1.7' / '1.6' sont des majeures valides (auparavant rejetées → « v? »).
// Majeure de cette page ; '1.7' / '1.6' sont des majeures valides
// (auparavant rejetées → « v? »).
$rawMajorVersion = $this->getMajorVersion();
$majorVersion = \PrestaFlow\Library\Visual\VisualTag::majorFromVersion(
is_string($this->globals['PS_VERSION'] ?? null) ? $this->globals['PS_VERSION'] : null
) ?? \PrestaFlow\Library\Visual\VisualTag::majorFromVersion(
is_scalar($rawMajorVersion) ? (string) $rawMajorVersion : null
);

Expand Down
1 change: 1 addition & 0 deletions src/Pages/FrontOfficePage.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class FrontOfficePage extends CommonPage

public function __construct(string $locale, string $patchVersion, array $globals, array $customs = [])
{
$this->initVersion(patchVersion: $patchVersion, globals: $globals);
$this->globals = $globals;
$this->customs = array_merge($this->customs, $customs);
$this->initLocale(locale: $locale);
Expand Down
6 changes: 4 additions & 2 deletions src/Scenarios/Scenario.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ public function __construct($testSuite, $params = [])
}

$locale = $this->globals['LOCALE'] ?? $testSuite->getLocale();
$versions = $this->globals['PATCH_VERSION'] ?? $testSuite->getVersions();

$this->setVersions(versions: $versions);
$this->setVersions(versions: $testSuite->getVersions());
if (is_string($this->globals['PATCH_VERSION'] ?? null) && $this->globals['PATCH_VERSION'] !== '') {
$this->exctractVersions($this->globals['PATCH_VERSION']);
}
$this->setLocale(locale: $locale);
$this->steps($testSuite);
}
Expand Down
5 changes: 5 additions & 0 deletions src/Tests/TestsSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ public function __construct(bool $loadGlobals = true, bool $getBrowser = true)
{
if ($loadGlobals) {
$this->loadGlobals();
} else {
// Une suite a toujours une version ($psVersion, onVersion, env,
// sinon 8.1.0) : before() la vérifie, et il n'y a plus de repli
// implicite dans Version::getMajorVersion().
$this->resolveVersion();
}

$this->before(getBrowser: $getBrowser);
Expand Down
34 changes: 28 additions & 6 deletions src/Traits/ImportPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace PrestaFlow\Library\Traits;

use PrestaFlow\Library\Exceptions\InvalidVersionException;

trait ImportPage
{
public function importPage($pageName, $userAgent = 'PrestaFlow', $globals = null, $domain = '\\PrestaFlow\\Library')
{
$pageClass = $domain.'\\Pages\\v'.$this->getMajorVersion(namespace: true).'\\'.$pageName.'\\Page';

if ($globals === null || !is_array($globals)) {
$globals = $this->globals;
} else {
Expand All @@ -27,7 +27,18 @@ public function importPage($pageName, $userAgent = 'PrestaFlow', $globals = null
}

$locale = $globals['LOCALE'] ?? $this->getLocale();
$patchVersion = $globals['PATCH_VERSION'] ?? $this->getPatchVersion();
$patchVersion = $globals['PATCH_VERSION'] ?? null;
if (!is_string($patchVersion) || $patchVersion === '') {
$patchVersion = (string) ($this->getPatchVersion() ?? '');
}

$pageClass = $domain.'\\Pages\\v'.$this->pageNamespaceVersion($patchVersion).'\\'.$pageName.'\\Page';

if (!class_exists($pageClass)) {
throw new InvalidVersionException(
'Page ' . $pageName . ' introuvable pour PrestaShop ' . $patchVersion . ' (' . $pageClass . ') : version non prise en charge ou nom de page erroné.'
);
}

$customs = [];
if (isset($this->customs) && is_array($this->customs)) {
Expand All @@ -38,10 +49,9 @@ public function importPage($pageName, $userAgent = 'PrestaFlow', $globals = null
$pageInstance->setGlobals($globals);
$pageInstance->setUserAgent($userAgent);
$pageInstance->setLocale(locale: $locale);
$pageInstance->setPatchVersion($patchVersion);
$pageInstance->setMinorVersion($this->getMinorVersion());
$pageInstance->setMajorVersion($this->getMajorVersion());

// La page a pris sa version à sa construction (patchVersion) ;
// initTranslations() la recalcule à partir du même patch.
$pageInstance->initTranslations(
locale: $locale,
patchVersion: $pageInstance->getPatchVersion(),
Expand All @@ -60,4 +70,16 @@ public function importPage($pageName, $userAgent = 'PrestaFlow', $globals = null
fwrite(STDERR, $pageInstance->lastThemeWarning . PHP_EOL);
}
}

/** Segment de namespace (7, 8, 9…) d'une version '1.7.8.11' / '9.2.0'. */
private function pageNamespaceVersion(string $patchVersion): string
{
if ($patchVersion === '') {
return (string) $this->getMajorVersion(namespace: true);
}

$major = self::parseVersions($patchVersion)['majorVersion'];

return str_starts_with($major, '1.') ? substr($major, 2) : $major;
}
}
Loading
Loading