diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 95ccd8e..ceecaf4 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -164,7 +164,10 @@ jobs: echo "JS tests skipped: no package.json + tests/js/ yet" exit 0 fi - npm ci + # Skip le download des browsers Playwright : ce job ne fait que node:test+jsdom. + # (@playwright/test est en devDep pour l'E2E, mais ses browsers ne servent + # qu'a tests/e2e via le conteneur officiel.) + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci npm run test:js auto-merge: diff --git a/.gitignore b/.gitignore index d7099b1..3f40d20 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,12 @@ node_modules/ npm-debug.log yarn-error.log +# === Playwright (E2E) === +playwright-report/ +test-results/ +/blob-report/ +.last-run.json + # === Docker volumes locaux === /docker-data/ diff --git a/package-lock.json b/package-lock.json index 25a3089..e3cedf4 100755 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "wakdo", "version": "0.0.0", "devDependencies": { + "@playwright/test": "1.49.1", "jsdom": "^26.0.0" } }, @@ -140,6 +141,22 @@ "node": ">=18" } }, + "node_modules/@playwright/test": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -216,6 +233,21 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -338,6 +370,38 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index eec3096..04c413a 100755 --- a/package.json +++ b/package.json @@ -4,9 +4,11 @@ "private": true, "description": "Wakdo - tests front borne (kiosk). Back-office PHP teste via PHPUnit (phpunit.phar). NB: pas de \"type\":\"module\" a la racine -> les .js du depot (hooks .claude, _byan, bin) restent CommonJS. L'ESM est declare localement la ou il s'applique (src/public/borne/assets/js, tests/js).", "scripts": { - "test:js": "node --test tests/js/" + "test:js": "node --test tests/js/", + "test:e2e": "playwright test" }, "devDependencies": { + "@playwright/test": "1.49.1", "jsdom": "^26.0.0" } } diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..227d32e --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,23 @@ +// Configuration Playwright (E2E borne). CommonJS : la racine n'est pas "type:module". +// La stack est montee a part (tests/e2e/run.sh) ; BASE_URL pointe vers wakdo-web. +const { defineConfig, devices } = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: './tests/e2e', + // Headless : tourne sur serveur sans ecran (dans le conteneur Playwright officiel). + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [['list'], ['html', { open: 'never', outputFolder: 'playwright-report' }]], + use: { + // run.sh fixe BASE_URL (hostname .test, joignable via --add-host). + baseURL: process.env.BASE_URL || 'http://kiosk.wakdo.test', + headless: true, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + ], +}); diff --git a/tests/e2e/borne.spec.js b/tests/e2e/borne.spec.js new file mode 100644 index 0000000..cd48177 --- /dev/null +++ b/tests/e2e/borne.spec.js @@ -0,0 +1,58 @@ +// Parcours E2E borne : welcome -> categories -> produit -> ajout panier -> panier +// -> paiement -> confirmation. La stack est montee a part (run.sh) ; le panier vit +// dans localStorage (meme origine), donc on peut naviguer par goto sans perdre l'etat. +const { test, expect } = require('@playwright/test'); + +test('parcours borne : de l\'accueil a la confirmation de commande', async ({ page }) => { + + await test.step('accueil -> categories', async () => { + await page.goto('/index.html'); + await expect(page).toHaveTitle(/Bienvenue/); + await expect(page.locator('#welcome-heading')).toBeVisible(); + // CTA "sur place" -> categories.html?mode=sur-place + await page.locator('a[href*="categories.html?mode=sur-place"]').click(); + await expect(page).toHaveURL(/categories\.html/); + }); + + await test.step('categories -> produits', async () => { + await expect(page.locator('h1.categories-main__heading')).toBeVisible(); + // Categorie 2 = boissons : produits SIMPLES (la categorie 1 = menus, qui rendent + // un autre gabarit a slots, sans bouton d'ajout direct). + await page.locator('a[href="products.html?category=2"]').click(); + await expect(page).toHaveURL(/products\.html\?category=2/); + }); + + await test.step('produits -> fiche produit', async () => { + // Cartes rendues par JS depuis le JSON : auto-wait sur la 1re carte. + const firstCard = page.locator('#products-grid a.product-card').first(); + await expect(firstCard).toBeVisible(); + await firstCard.click(); + await expect(page).toHaveURL(/product\.html\?id=/); + }); + + await test.step('ajout au panier', async () => { + const addBtn = page.locator('#add-to-cart-btn'); + await expect(addBtn).toBeVisible(); + await addBtn.click(); + // Feedback visuel "Ajoute !" (page-product.js) ; l'ecriture localStorage est synchrone. + await expect(addBtn).toHaveText(/Ajoute/); + }); + + await test.step('panier : recapitulatif', async () => { + await page.goto('/cart.html'); + await expect(page.locator('#cart-summary')).toBeVisible(); + await expect(page.locator('#cart-list li')).toHaveCount(1); + // Total calcule, plus le placeholder "—". + await expect(page.locator('#total-ttc')).not.toHaveText('—'); + await page.locator('#pay-btn').click(); + await expect(page).toHaveURL(/payment\.html/); + }); + + await test.step('paiement -> confirmation', async () => { + await page.locator('#pay-card').click(); + await expect(page).toHaveURL(/confirmation\.html/); + await expect(page.locator('.confirmation-banner__title')).toHaveText(/Commande confirmee/); + // Numero de commande genere (plus le placeholder). + await expect(page.locator('#order-number')).not.toHaveText('—'); + }); +}); diff --git a/tests/e2e/docker-compose.e2e.yml b/tests/e2e/docker-compose.e2e.yml new file mode 100644 index 0000000..1cb77c1 --- /dev/null +++ b/tests/e2e/docker-compose.e2e.yml @@ -0,0 +1,17 @@ +# Override JETABLE pour l'E2E : neutralise les container_name fixes du compose afin de +# monter une stack isolee (`-p wakdoe2e`) en parallele d'une stack existante, sans +# collision de noms. N'est PAS un compose de deploiement ; sert uniquement a run.sh. +services: + wakdo-db: + container_name: wakdoe2e-db + wakdo-migrate: + container_name: wakdoe2e-migrate + wakdo-app: + container_name: wakdoe2e-app + wakdo-web: + container_name: wakdoe2e-web + # Pas de port hote pour l'E2E : Playwright joint wakdo-web par le reseau interne + # (--add-host). Evite tout conflit de port sur l'hote. + ports: !reset [] + wakdo-cron: + container_name: wakdoe2e-cron diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh new file mode 100755 index 0000000..b8a9056 --- /dev/null +++ b/tests/e2e/run.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# E2E borne : monte une stack JETABLE isolee, lance Playwright (conteneur officiel, +# headless) contre elle, puis demonte tout. Ne touche a aucune stack existante. +# +# tests/e2e/run.sh +# +# Pre-requis : Docker. Aucune dependance Node/Playwright sur l'hote (tout en conteneur). +# +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +PROJECT=wakdoe2e +PW_VERSION=1.49.1 # doit matcher devDependencies["@playwright/test"] de package.json +NET="${PROJECT}_wakdo_internal" + +ENVFILE="$(mktemp)" +cp .env.example "$ENVFILE" # template local-first : marche tel quel (valeurs dev) +# Hostnames de TEST en .test (pas .localhost) : Chromium/curl resolvent *.localhost en +# dur vers 127.0.0.1 (RFC 6761) et ignorent --add-host. .test n'est pas special -> joignable. +perl -pi -e 's/^APP_HOST_KIOSK=.*/APP_HOST_KIOSK=kiosk.wakdo.test/; s/^APP_HOST_ADMIN=.*/APP_HOST_ADMIN=admin.wakdo.test/;' "$ENVFILE" +COMPOSE="docker compose -p $PROJECT --env-file $ENVFILE -f docker-compose.yml -f tests/e2e/docker-compose.e2e.yml" + +cleanup() { echo "[e2e] teardown"; $COMPOSE down -v >/dev/null 2>&1 || true; rm -f "$ENVFILE"; } +trap cleanup EXIT + +echo "[e2e] build + up stack jetable ($PROJECT)" +$COMPOSE up -d --build + +echo "[e2e] attente migrate (completion)" +for _ in $(seq 1 40); do + st="$(docker inspect -f '{{.State.Status}}' wakdoe2e-migrate 2>/dev/null || echo NA)" + code="$(docker inspect -f '{{.State.ExitCode}}' wakdoe2e-migrate 2>/dev/null || echo NA)" + [ "$st" = "exited" ] && [ "$code" = "0" ] && { echo "[e2e] migrate OK"; break; } + [ "$st" = "exited" ] && [ "$code" != "0" ] && { echo "[e2e] migrate ECHEC ($code)"; docker logs wakdoe2e-migrate; exit 1; } + sleep 2 +done + +echo "[e2e] attente web healthy" +for _ in $(seq 1 40); do + [ "$(docker inspect -f '{{.State.Health.Status}}' wakdoe2e-web 2>/dev/null || echo NA)" = "healthy" ] && break + sleep 2 +done + +WEB_IP="$(docker inspect -f "{{(index .NetworkSettings.Networks \"$NET\").IPAddress}}" wakdoe2e-web)" +echo "[e2e] web @ $WEB_IP ($NET)" + +echo "[e2e] Playwright (conteneur officiel v$PW_VERSION)" +docker run --rm \ + --network "$NET" \ + --add-host "kiosk.wakdo.test:$WEB_IP" \ + --add-host "admin.wakdo.test:$WEB_IP" \ + -v "$ROOT":/work -w /work \ + -e BASE_URL="http://kiosk.wakdo.test" \ + -e CI=1 \ + "mcr.microsoft.com/playwright:v${PW_VERSION}-jammy" \ + bash -c "npm install --no-audit --no-fund --silent && npx playwright test"