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
16 changes: 16 additions & 0 deletions .env.flashlight.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Values for a Flashlight shop. Copy to .env.flashlight and adjust the port.
#
# The back office folder and credentials are fixed by the image, so unlike a
# hand-made install there is nothing machine-specific to discover here.
PRESTAFLOW_FO_URL=http://localhost:8092/
PRESTAFLOW_BO_URL=http://localhost:8092/admin-dev/
PRESTAFLOW_BO_EMAIL=admin@prestashop.com
PRESTAFLOW_BO_PASSWD=prestashop
PRESTAFLOW_LOCALE=en

# One of these per shop:
# 1.7.8.11 -> port 8017, theme classic
# 8.2.8 -> port 8082, theme classic
# 9.2.0 -> port 8092, theme hummingbird
PRESTAFLOW_PS_VERSION=9.2.0
PRESTAFLOW_THEME=hummingbird
130 changes: 130 additions & 0 deletions .github/workflows/live-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: Live smoke

# Runs the smoke suites against a throwaway Flashlight shop, one job per
# PrestaShop version. Both suites run in every job, and a red in either one
# fails the job.
#
# Every row is expected to be GREEN. That was not true when this workflow
# landed: BackOfficeSmoke was red everywhere on the version cross-check and
# additionally red on 1.7 and 8.2 for the login-error read and logout. Those
# three were page-object defects, not absences of support, and they are fixed —
# see the commit that touched src/Pages/BackOfficePage.php. A red row is now a
# regression and should be read as one.
#
# `fail-fast: false` still keeps one red row from cancelling the others: the
# point of the matrix is to say WHICH versions diverged, not merely that one
# did. Nothing here is `continue-on-error`.
#
# Measured 2026-09-24 against Flashlight, 7 runs per combination:
#
# FrontOfficeSmoke 7/7 green on 1.7.8.11, 8.2.8 and 9.2.0, classic and
# hummingbird.
# BackOfficeSmoke 7/7 green on the same four combinations. The bad-password
# step, which used to land both ways on 1.7 and 8.2, was
# green in all 28 runs.

on:
push:
branches: [main]
pull_request:

jobs:
smoke:
name: Smoke (PrestaShop ${{ matrix.ps }}, ${{ matrix.theme }})
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
include:
- ps: '1.7.8.11'
service: ps17
port: 8017
theme: classic
- ps: '8.2.8'
service: ps82
port: 8082
theme: classic
- ps: '9.2.0'
service: ps92
port: 8092
theme: hummingbird
# Shop 2 of the same container, on its own port and its own theme.
# Without this row "9.2 passes" would only ever mean "hummingbird
# passes", and a theme-aware selector that silently matched on one
# theme would never be caught.
- ps: '9.2.0'
service: ps92
port: 8093
theme: classic

steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: none
tools: composer:v2

- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

- name: Cache Composer packages
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-8.3-${{ hashFiles('composer.json') }}
restore-keys: composer-8.3-

- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist

- name: Start the shop
run: docker compose up ${{ matrix.service }} -d

# Probe /admin-dev/, not /. The front office answers 200 well before the
# post-scripts that provision the second shop have finished, so polling /
# would let the suite start against a half-provisioned shop. Flashlight
# redirects /admin-dev/ to the login page (302) once the container is
# really serving.
- name: Wait for the shop
run: |
for i in $(seq 1 60); do
code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${{ matrix.port }}/admin-dev/" || true)
if [ "$code" = "302" ] || [ "$code" = "200" ]; then
echo "back office answers $code — shop is up"; exit 0
fi
echo "back office answers ${code:-000}, waiting..."
sleep 5
done
echo "shop never came up"; docker compose logs ${{ matrix.service }}; exit 1

# Both suites run even when the first one is red, so one job reports the
# whole inventory rather than stopping at the front office. Each command's
# status is captured separately and the step exits non-zero if either
# failed, so neither suite can hide the other's result.
- name: Run the smoke suites
env:
PRESTAFLOW_FO_URL: http://localhost:${{ matrix.port }}/
PRESTAFLOW_BO_URL: http://localhost:${{ matrix.port }}/admin-dev/
PRESTAFLOW_BO_EMAIL: admin@prestashop.com
PRESTAFLOW_BO_PASSWD: prestashop
PRESTAFLOW_PS_VERSION: ${{ matrix.ps }}
PRESTAFLOW_THEME: ${{ matrix.theme }}
PRESTAFLOW_LOCALE: en
run: |
status=0
php bin/prestaflow run src/Tests/Suites/Smoke/FrontOfficeSmoke.php || status=1
php bin/prestaflow run src/Tests/Suites/Smoke/BackOfficeSmoke.php || status=1
exit $status

- name: Upload failure screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots-${{ matrix.ps }}-${{ matrix.theme }}
path: prestaflow/screens/
if-no-files-found: ignore
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ prestaflow/
.vscode/settings.json
datas/.broswer
.phpunit.cache/
.env.flashlight
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,44 @@ class MigrationBaselineSuite extends TestsSuite
Resolution priority: fluent `onVersion()` → `$psVersion` property → `PRESTAFLOW_PS_VERSION` env → default `8.1.0`.

`onVersion()` throws `InvalidArgumentException` on malformed input (expected format: `1.7`, `1.7.8`, `1.7.8.11`, `9`, `9.0`, `9.0.1`, etc.).

## Run a suite against a throwaway shop

`docker-compose.yml` boots a disposable PrestaShop from the official
[Flashlight](https://github.com/PrestaShop/prestashop-flashlight) images — one
service per version, each with its own database and its own port:

| Service | PrestaShop | Shop 1 | Shop 2 | Shop 1 theme | Shop 2 theme |
|---|---|---|---|---|---|
| `ps17` | 1.7.8.11 | 8017 | 8018 | classic | classic |
| `ps82` | 8.2.8 | 8082 | 8083 | classic | classic |
| `ps92` | 9.2.0 | 8092 | 8093 | hummingbird | classic |

The 9.2 container therefore gives you both themes from a single boot —
hummingbird on 8092, classic on 8093 — which is what you want for checking
that a theme-aware selector resolves per theme.

```bash
docker compose up ps92 -d
cp .env.flashlight.example .env.flashlight # then point your run at it
php bin/prestaflow run src/Tests/Suites/Smoke/FrontOfficeSmoke.php
```

Wait for `/admin-dev/` to answer `302` before running a suite, not for `/` to
answer `200`: the front office is served well before the post-scripts that
provision the second shop have finished. A failing post-script does **not**
stop the container, so a healthy container is not proof of a provisioned shop
— the smoke suite is.

Full walkthrough, including tear-down and the two traps worth knowing:
[Testing against a throwaway shop](docs/testing-with-flashlight.md).

The `Live smoke` workflow runs that same suite against all three versions on
every push to `main` and every pull request.

**The 1.7 and 8.2 rows of the live-smoke matrix are expected to fail today.**
Their page objects are nine-line stubs inheriting the v9 selectors, so the
matrix is reporting an absence of support rather than a regression. Each
failure names the page that differs, which is the inventory the v7/v8 work
needs. The rows are deliberately not skipped and not marked
`continue-on-error`; `fail-fast: false` keeps them from cancelling the 9.2 row.
99 changes: 99 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Throwaway PrestaShop shops from the official Flashlight images.
#
# One shop and one database per version. Bring up a single shop at a time:
# docker compose up ps92 -d
# Its database starts with it through depends_on.
#
# Tags carry a suffix on purpose. The bare `9.2.0` and `8.2.8` tags do not
# exist on the registry — a previous attempt at this file pinned `9.0.1` and
# could never start.
services:
db17:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: prestashop
MARIADB_DATABASE: prestashop
MARIADB_USER: prestashop
MARIADB_PASSWORD: prestashop
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect"]
interval: 5s
timeout: 5s
retries: 20

ps17:
image: prestashop/prestashop-flashlight:1.7.8.11-nginx
depends_on:
db17:
condition: service_healthy
environment:
PS_DOMAIN: localhost:8017
SECOND_SHOP_PORT: "8018"
MYSQL_HOST: db17
DEBUG_MODE: "false"
POST_SCRIPTS_DIR: /tmp/post-scripts
volumes:
- ./docker/post-scripts:/tmp/post-scripts:ro
ports:
- "8017:80"
- "8018:80" # the second shop

db82:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: prestashop
MARIADB_DATABASE: prestashop
MARIADB_USER: prestashop
MARIADB_PASSWORD: prestashop
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect"]
interval: 5s
timeout: 5s
retries: 20

ps82:
image: prestashop/prestashop-flashlight:8.2.8-nginx
depends_on:
db82:
condition: service_healthy
environment:
PS_DOMAIN: localhost:8082
SECOND_SHOP_PORT: "8083"
MYSQL_HOST: db82
DEBUG_MODE: "false"
POST_SCRIPTS_DIR: /tmp/post-scripts
volumes:
- ./docker/post-scripts:/tmp/post-scripts:ro
ports:
- "8082:80"
- "8083:80" # the second shop

db92:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: prestashop
MARIADB_DATABASE: prestashop
MARIADB_USER: prestashop
MARIADB_PASSWORD: prestashop
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect"]
interval: 5s
timeout: 5s
retries: 20

ps92:
image: prestashop/prestashop-flashlight:9.2.0-nginx
depends_on:
db92:
condition: service_healthy
environment:
PS_DOMAIN: localhost:8092
SECOND_SHOP_PORT: "8093"
MYSQL_HOST: db92
DEBUG_MODE: "false"
POST_SCRIPTS_DIR: /tmp/post-scripts
volumes:
- ./docker/post-scripts:/tmp/post-scripts:ro
ports:
- "8092:80"
- "8093:80" # the second shop, on its own port rather than a virtual URI
94 changes: 94 additions & 0 deletions docker/post-scripts/10-second-shop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/sh
set -eu

echo "* Provisioning a second shop..."

cat > /tmp/second-shop.php <<'PHP'
<?php
require_once '/var/www/html/config/config.inc.php';

$src = 1;

// A second shop, its group, and its URL on a dedicated port.
$group = new ShopGroup();
$group->name = 'Group2';
$group->active = true;
$group->add();

$shop = new Shop();
$shop->name = 'Shop2';
$shop->id_shop_group = (int) $group->id;
$shop->id_category = (int) Configuration::get('PS_HOME_CATEGORY');
$shop->theme_name = 'classic';
$shop->active = true;
$shop->add();

$url = new ShopUrl();
$url->id_shop = (int) $shop->id;
$secondPort = getenv('SECOND_SHOP_PORT') ?: '8093';
$host = preg_replace('/:\d+$/', '', (string) Configuration::get('PS_SHOP_DOMAIN'));
$url->domain = $url->domain_ssl = $host . ':' . $secondPort;
// A DEDICATED PORT, not a virtual URI. PrestaShop discriminates shops by
// domain including the port, so localhost:8093 is a distinct shop — and its
// assets resolve at the root, needing no rewrite at all.
//
// The virtual-URI approach this replaced cannot work here: it relies on
// Tools::generateHtaccess(), and every Flashlight tag we use is -nginx, which
// never reads .htaccess. Verified 2026-09-24: /shop2/themes/...css returned
// 404 while the same file at the root returned 200. There is no -apache tag
// for 1.7.8.11, 8.2.8 or 9.2.0 (only 9.0.3 has one), so switching flavour is
// not an option either.
$url->physical_uri = '/';
$url->virtual_uri = '';
$url->main = true;
$url->active = true;
$url->add();

// copyShopData() reads Tools::getValue('categoryBox'): outside an HTTP request
// that returns false and count(false) is fatal on PHP 8. Prime it with the
// source shop's categories, which is what the Multistore form would post.
$rows = Db::getInstance()->executeS(
'SELECT id_category FROM ' . _DB_PREFIX_ . 'category_shop WHERE id_shop = ' . (int) $src
);
$categories = array_map('intval', array_column($rows ?: [], 'id_category'));
$_POST['categoryBox'] = $_REQUEST['categoryBox'] = $categories;

// The same 26 keys the Multistore form offers.
$keys = [
'carrier', 'cms', 'contact', 'country', 'currency', 'discount', 'employee',
'image', 'lang', 'manufacturer', 'module', 'hook_module', 'meta_lang',
'product', 'product_attribute', 'stock_available', 'store',
'webservice_account', 'attribute_group', 'feature', 'group',
'tax_rules_group', 'supplier', 'zone', 'cart_rule',
];
$shop->copyShopData($src, array_fill_keys($keys, 'on'));
$shop->associateSuperAdmins();

array_unshift($categories, (int) Configuration::get('PS_ROOT_CATEGORY'));
Category::updateFromShop(array_values(array_unique($categories)), (int) $shop->id);

// Module-owned data travels through a hook, not through copyShopData().
foreach ((array) Hook::getHookModuleExecList('actionShopDataDuplication') as $m) {
Hook::exec('actionShopDataDuplication', [
'old_id_shop' => $src,
'new_id_shop' => (int) $shop->id,
], (int) $m['id_module']);
}

// Multistore is enabled, and its back-office screens must exist with it:
// setting the flag without activating the tabs gives a shop that cannot be
// managed, which is exactly how the previous hand-made container ended up.
Configuration::updateValue('PS_MULTISHOP_FEATURE_ACTIVE', 1);
Db::getInstance()->execute(
'UPDATE ' . _DB_PREFIX_ . "tab SET active = 1 WHERE class_name IN ('AdminShopGroup','AdminShopUrl')"
);

// Nothing to rewrite: the second shop lives at the root of its own port.

echo 'second shop id=' . (int) $shop->id . PHP_EOL;
PHP

php /tmp/second-shop.php
rm -f /tmp/second-shop.php

echo "✅ Second shop provisioned"
Loading
Loading