feat(borne): modale allergenes generale (14 INCO) sur carte et fiche + harnais tests JS (P3) (#36)
This commit is contained in:
parent
ed392d4c14
commit
1ecd78324c
12 changed files with 1015 additions and 4 deletions
|
|
@ -142,14 +142,39 @@ jobs:
|
|||
# du correctif : plus aucun skip silencieux des chemins securite.
|
||||
php phpunit.phar -c phpunit.xml --fail-on-skipped
|
||||
|
||||
js-tests:
|
||||
# Tests du front borne (kiosk) : node:test + jsdom, sans navigateur.
|
||||
# GARDE : ne s'active que si package.json + tests/js/ existent.
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node.js 20
|
||||
run: |
|
||||
set -eu
|
||||
# Node 20 epingle via NodeSource (self-contained, comme les .phar/gitleaks)
|
||||
# plutot que l'apt bookworm (18.x, limite basse pour jsdom). Reproductible.
|
||||
apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null
|
||||
apt-get install -y -qq nodejs >/dev/null
|
||||
node --version && npm --version
|
||||
- name: Install deps + run kiosk JS tests
|
||||
run: |
|
||||
set -eu
|
||||
if [ ! -f package.json ] || [ ! -d tests/js ]; then
|
||||
echo "JS tests skipped: no package.json + tests/js/ yet"
|
||||
exit 0
|
||||
fi
|
||||
npm ci
|
||||
npm run test:js
|
||||
|
||||
auto-merge:
|
||||
# Fusion automatique OPT-IN : poser le label `auto-merge` sur la PR.
|
||||
# Ne s'execute que si les 3 checks passent (needs).
|
||||
# Ne s'execute que si tous les checks requis passent (needs).
|
||||
# IMPORTANT : le filtrage par label se fait DANS le step via l'API, pas dans
|
||||
# `if:` — l'expression contains(github.event.pull_request.labels.*.name, ...)
|
||||
# de Forgejo n'est pas fiable (elle s'evalue a vrai meme sans label, ce qui
|
||||
# fusionnait toute PR verte). La verification shell sur l'API est le vrai gate.
|
||||
needs: [secret-scan, php-lint, static-tests]
|
||||
needs: [secret-scan, php-lint, static-tests, js-tests]
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: docker
|
||||
steps:
|
||||
|
|
|
|||
545
package-lock.json
generated
Executable file
545
package-lock.json
generated
Executable file
|
|
@ -0,0 +1,545 @@
|
|||
{
|
||||
"name": "wakdo",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wakdo",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"jsdom": "^26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"nwsapi": "^2.2.16",
|
||||
"parse5": "^7.2.1",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.1.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.24",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
|
||||
"integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
package.json
Executable file
12
package.json
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "wakdo",
|
||||
"version": "0.0.0",
|
||||
"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/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^26.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1437,6 +1437,122 @@ button {
|
|||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Allergenes — bouton "i" + modale generale (14 INCO)
|
||||
* Info reglementaire generale (pas un calcul par produit). La modale reutilise
|
||||
* le pattern overlay du composer ; z-index 220 pour passer au-dessus de lui.
|
||||
* ============================================================ */
|
||||
|
||||
/* Le bouton "i" se superpose au coin de l'image de la carte. */
|
||||
.product-card__image-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.allergen-info-btn {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-brand-dark);
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-brand-dark);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-style: italic;
|
||||
font-size: var(--font-size-base);
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.product-card__image-wrap .allergen-info-btn {
|
||||
position: absolute;
|
||||
top: var(--space-2);
|
||||
right: var(--space-2);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.product-detail__info .allergen-info-btn {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.allergen-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 220;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-4);
|
||||
animation: composer-fade-in var(--transition-base) both;
|
||||
}
|
||||
|
||||
.allergen-modal {
|
||||
position: relative;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-6);
|
||||
animation: composer-slide-up var(--transition-base) both;
|
||||
}
|
||||
|
||||
.allergen-modal-close {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.allergen-modal-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.allergen-modal-intro {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.allergen-modal-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.allergen-modal-list li {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--color-bg-page);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.allergen-name {
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.allergen-desc {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.composer-card__price {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
|
|
|
|||
132
src/public/borne/assets/js/allergens.js
Normal file
132
src/public/borne/assets/js/allergens.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* allergens.js — Modale GENERALE d'information allergenes (front borne).
|
||||
*
|
||||
* Information reglementaire (UE INCO 1169/2011) presentee au client : la liste
|
||||
* des 14 allergenes a declaration obligatoire. C'est une info GENERALE (pas un
|
||||
* calcul par produit) ; le mapping ingredient_allergen par produit reste differe.
|
||||
*
|
||||
* CSP 'self' : aucun script inline, aucun handler inline. Le DOM est construit par
|
||||
* l'API (createElement/textContent) ; textContent neutralise toute injection.
|
||||
* Les donnees viennent de data.js (loadAllergens) : liste fixe en P5, /api/allergens
|
||||
* au swap P4. openAllergenModal prend la liste en parametre pour rester independant
|
||||
* de la couche de chargement (et testable sans fetch).
|
||||
*/
|
||||
|
||||
const OVERLAY_CLASS = 'allergen-modal-overlay';
|
||||
|
||||
/* Reference stable du handler clavier pour pouvoir le retirer a la fermeture. */
|
||||
function onKeydown(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeAllergenModal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le bouton "i" qui ouvre la modale. `onOpen` est appele au clic ;
|
||||
* la propagation est stoppee pour ne pas declencher le clic de la carte produit
|
||||
* (sur la carte, le bouton est superpose a une zone cliquable).
|
||||
* @param {() => void} onOpen
|
||||
* @returns {HTMLButtonElement}
|
||||
*/
|
||||
export function buildAllergenInfoButton(onOpen) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'allergen-info-btn';
|
||||
btn.setAttribute('aria-label', 'Informations allergenes');
|
||||
btn.title = 'Informations allergenes';
|
||||
btn.textContent = 'i';
|
||||
btn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (typeof onOpen === 'function') {
|
||||
onOpen();
|
||||
}
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ouvre la modale generale listant les allergenes fournis. Idempotent : une
|
||||
* eventuelle modale ouverte est d'abord fermee (pas de doublon empile).
|
||||
* @param {Array<{id:number, name:string, description?:string}>} allergens
|
||||
* @returns {HTMLElement} l'overlay cree
|
||||
*/
|
||||
export function openAllergenModal(allergens) {
|
||||
closeAllergenModal();
|
||||
|
||||
const list = Array.isArray(allergens) ? allergens : [];
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = OVERLAY_CLASS;
|
||||
overlay.setAttribute('role', 'dialog');
|
||||
overlay.setAttribute('aria-modal', 'true');
|
||||
overlay.setAttribute('aria-label', 'Informations allergenes');
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'allergen-modal';
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.type = 'button';
|
||||
closeBtn.className = 'allergen-modal-close';
|
||||
closeBtn.setAttribute('aria-label', 'Fermer');
|
||||
closeBtn.textContent = 'x';
|
||||
closeBtn.addEventListener('click', closeAllergenModal);
|
||||
modal.appendChild(closeBtn);
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'allergen-modal-title';
|
||||
title.textContent = 'Allergenes';
|
||||
modal.appendChild(title);
|
||||
|
||||
const intro = document.createElement('p');
|
||||
intro.className = 'allergen-modal-intro';
|
||||
intro.textContent = 'Les 14 allergenes a declaration obligatoire (reglement UE INCO 1169/2011). Pour toute question, demandez en caisse.';
|
||||
modal.appendChild(intro);
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
ul.className = 'allergen-modal-list';
|
||||
for (const allergen of list) {
|
||||
const li = document.createElement('li');
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'allergen-name';
|
||||
name.textContent = String(allergen.name ?? '');
|
||||
li.appendChild(name);
|
||||
|
||||
if (allergen.description) {
|
||||
const desc = document.createElement('span');
|
||||
desc.className = 'allergen-desc';
|
||||
desc.textContent = ' - ' + String(allergen.description);
|
||||
li.appendChild(desc);
|
||||
}
|
||||
|
||||
ul.appendChild(li);
|
||||
}
|
||||
modal.appendChild(ul);
|
||||
|
||||
overlay.appendChild(modal);
|
||||
|
||||
// Clic sur le fond (hors du panneau) = fermeture ; clic dans le panneau, non.
|
||||
overlay.addEventListener('click', (event) => {
|
||||
if (event.target === overlay) {
|
||||
closeAllergenModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', onKeydown);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ferme la modale si elle est ouverte et retire le handler clavier. Sans effet
|
||||
* si aucune modale n'est ouverte (sur appel ou Echap repete).
|
||||
*/
|
||||
export function closeAllergenModal() {
|
||||
const existing = document.querySelector('.' + OVERLAY_CLASS);
|
||||
if (existing && existing.parentNode) {
|
||||
existing.parentNode.removeChild(existing);
|
||||
}
|
||||
document.removeEventListener('keydown', onKeydown);
|
||||
}
|
||||
|
|
@ -19,6 +19,9 @@
|
|||
* ----------------------------------------------------------------------- */
|
||||
const CATEGORIES_URL = 'data/categories.json';
|
||||
const PRODUCTS_URL = 'data/produits.json';
|
||||
/* Liste fixe des 14 allergenes INCO (info generale, modale borne). TODO(P4):
|
||||
* remplacer par '/api/allergens'. Le reste du fichier est API-agnostique. */
|
||||
const ALLERGENS_URL = 'data/allergens.json';
|
||||
|
||||
/** @type {Array|null} — in-memory cache to avoid repeated fetches */
|
||||
let _categoriesCache = null;
|
||||
|
|
@ -26,6 +29,9 @@ let _categoriesCache = null;
|
|||
/** @type {Object|null} */
|
||||
let _productsCache = null;
|
||||
|
||||
/** @type {Array|null} */
|
||||
let _allergensCache = null;
|
||||
|
||||
/**
|
||||
* Fetches and caches the categories list.
|
||||
* @returns {Promise<Array>}
|
||||
|
|
@ -50,6 +56,18 @@ export async function loadProducts() {
|
|||
return _productsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and caches the 14 INCO allergens (general info modal).
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
export async function loadAllergens() {
|
||||
if (_allergensCache) return _allergensCache;
|
||||
const res = await fetch(ALLERGENS_URL);
|
||||
if (!res.ok) throw new Error(`Failed to load allergens: HTTP ${res.status}`);
|
||||
_allergensCache = await res.json();
|
||||
return _allergensCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of products for a given category slug.
|
||||
* Returns [] if the slug is not found.
|
||||
|
|
|
|||
4
src/public/borne/assets/js/package.json
Normal file
4
src/public/borne/assets/js/package.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"//": "Marque les scripts du kiosk comme ESM (import/export) pour Node (tests). Le navigateur les charge via <script type=module> independamment de ce fichier. Scoping local : evite d'imposer type:module a la racine (qui casserait les hooks CommonJS).",
|
||||
"type": "module"
|
||||
}
|
||||
|
|
@ -15,10 +15,11 @@
|
|||
* 3. Redirect to products.html?category=<slug>
|
||||
*/
|
||||
|
||||
import { findProduct } from './data.js';
|
||||
import { findProduct, loadAllergens } from './data.js';
|
||||
import { addToCart, formatPrice, escHtml } from './state.js';
|
||||
import { refreshCartBadge } from './nav.js';
|
||||
import { openMenuComposer } from './page-product-menu.js';
|
||||
import { buildAllergenInfoButton, openAllergenModal } from './allergens.js';
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const productId = parseInt(params.get('id'), 10);
|
||||
|
|
@ -78,6 +79,18 @@ async function renderProduct() {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Bouton "i" allergenes (modale generale) dans le bloc info de la fiche.
|
||||
// Echec de chargement non bloquant : la fiche reste fonctionnelle.
|
||||
try {
|
||||
const allergens = await loadAllergens();
|
||||
const info = container.querySelector('.product-detail__info');
|
||||
if (info) {
|
||||
info.appendChild(buildAllergenInfoButton(() => openAllergenModal(allergens)));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('loadAllergens error:', e);
|
||||
}
|
||||
|
||||
document.getElementById('add-to-cart-btn').addEventListener('click', () => {
|
||||
addToCart({
|
||||
id: product.id,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@
|
|||
* On product card click, navigates to product.html?id=<id>&category=<slug>.
|
||||
*/
|
||||
|
||||
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG } from './data.js';
|
||||
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG, loadAllergens } from './data.js';
|
||||
import { formatPrice, escHtml } from './state.js';
|
||||
import { buildAllergenInfoButton, openAllergenModal } from './allergens.js';
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const categoryId = parseInt(params.get('category'), 10) || 1;
|
||||
|
|
@ -49,6 +50,15 @@ async function renderProducts() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Liste generale des allergenes (modale "i"). Chargee une fois, partagee par
|
||||
// toutes les cartes ; un echec ne doit pas casser l'affichage produits.
|
||||
let allergens = [];
|
||||
try {
|
||||
allergens = await loadAllergens();
|
||||
} catch (e) {
|
||||
console.error('loadAllergens error:', e);
|
||||
}
|
||||
|
||||
grid.innerHTML = '';
|
||||
products.forEach(product => {
|
||||
const card = document.createElement('a');
|
||||
|
|
@ -71,6 +81,12 @@ async function renderProducts() {
|
|||
<span class="product-card__price">${formatPrice(product.prix)}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Bouton "i" allergenes superpose a l'image ; son clic ouvre la modale
|
||||
// generale et ne declenche pas la navigation de la carte (stopPropagation).
|
||||
const infoBtn = buildAllergenInfoButton(() => openAllergenModal(allergens));
|
||||
card.querySelector('.product-card__image-wrap').appendChild(infoBtn);
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
|
|
|
|||
16
src/public/borne/data/allergens.json
Normal file
16
src/public/borne/data/allergens.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{ "id": 1, "name": "Cereales contenant du gluten", "description": "Ble, seigle, orge, avoine, epeautre, kamut et produits derives." },
|
||||
{ "id": 2, "name": "Crustaces", "description": "Et produits a base de crustaces." },
|
||||
{ "id": 3, "name": "Oeufs", "description": "Et produits a base d'oeufs." },
|
||||
{ "id": 4, "name": "Poissons", "description": "Et produits a base de poissons." },
|
||||
{ "id": 5, "name": "Arachides", "description": "Et produits a base d'arachides." },
|
||||
{ "id": 6, "name": "Soja", "description": "Et produits a base de soja." },
|
||||
{ "id": 7, "name": "Lait", "description": "Et produits a base de lait (y compris le lactose)." },
|
||||
{ "id": 8, "name": "Fruits a coque", "description": "Amandes, noisettes, noix, noix de cajou, pistaches et autres." },
|
||||
{ "id": 9, "name": "Celeri", "description": "Et produits a base de celeri." },
|
||||
{ "id": 10, "name": "Moutarde", "description": "Et produits a base de moutarde." },
|
||||
{ "id": 11, "name": "Graines de sesame", "description": "Et produits a base de graines de sesame." },
|
||||
{ "id": 12, "name": "Anhydride sulfureux et sulfites", "description": "En concentration de plus de 10 mg/kg ou 10 mg/l." },
|
||||
{ "id": 13, "name": "Lupin", "description": "Et produits a base de lupin." },
|
||||
{ "id": 14, "name": "Mollusques", "description": "Et produits a base de mollusques." }
|
||||
]
|
||||
110
tests/js/allergens.test.js
Normal file
110
tests/js/allergens.test.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Tests du module allergens du front borne (node:test + jsdom).
|
||||
*
|
||||
* Couvre le contrat de PR-C : la liste fixe des 14 allergenes INCO (data borne,
|
||||
* se branchera sur /api/allergens au swap P4), la construction du bouton "i", et
|
||||
* la modale GENERALE (ouverture, listing des 14, fermeture par bouton/overlay/
|
||||
* Escape, idempotence). DOM simule par jsdom : aucun navigateur requis.
|
||||
*/
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
import {
|
||||
buildAllergenInfoButton,
|
||||
openAllergenModal,
|
||||
closeAllergenModal,
|
||||
} from '../../src/public/borne/assets/js/allergens.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const allergensJsonPath = join(here, '../../src/public/borne/data/allergens.json');
|
||||
|
||||
function setupDom() {
|
||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
return dom;
|
||||
}
|
||||
|
||||
function loadAllergensFixture() {
|
||||
return JSON.parse(readFileSync(allergensJsonPath, 'utf8'));
|
||||
}
|
||||
|
||||
test('data/allergens.json liste exactement les 14 allergenes INCO', () => {
|
||||
const list = loadAllergensFixture();
|
||||
assert.ok(Array.isArray(list));
|
||||
assert.equal(list.length, 14);
|
||||
for (const a of list) {
|
||||
assert.equal(typeof a.id, 'number');
|
||||
assert.equal(typeof a.name, 'string');
|
||||
assert.ok(a.name.trim().length > 0);
|
||||
}
|
||||
const names = list.map((a) => a.name);
|
||||
assert.equal(new Set(names).size, 14, 'noms uniques');
|
||||
// Quelques jalons de la liste reglementaire (UE INCO 1169/2011 annexe II).
|
||||
const joined = names.join(' | ').toLowerCase();
|
||||
for (const expected of ['gluten', 'lait', 'arachide', 'soja', 'mollusque']) {
|
||||
assert.ok(joined.includes(expected), `attendu: ${expected}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('buildAllergenInfoButton cree un bouton "i" qui declenche onOpen', () => {
|
||||
setupDom();
|
||||
let opened = 0;
|
||||
const btn = buildAllergenInfoButton(() => { opened += 1; });
|
||||
|
||||
assert.equal(btn.tagName, 'BUTTON');
|
||||
assert.equal(btn.type, 'button');
|
||||
assert.ok(btn.className.includes('allergen-info-btn'));
|
||||
assert.ok(btn.getAttribute('aria-label'));
|
||||
|
||||
btn.click();
|
||||
assert.equal(opened, 1, 'le clic ouvre la modale');
|
||||
});
|
||||
|
||||
test('openAllergenModal affiche une modale listant les 14 allergenes', () => {
|
||||
setupDom();
|
||||
const list = loadAllergensFixture();
|
||||
const overlay = openAllergenModal(list);
|
||||
|
||||
assert.ok(document.body.contains(overlay));
|
||||
assert.equal(overlay.getAttribute('role'), 'dialog');
|
||||
assert.equal(overlay.getAttribute('aria-modal'), 'true');
|
||||
const items = overlay.querySelectorAll('.allergen-modal-list li');
|
||||
assert.equal(items.length, 14);
|
||||
assert.ok(overlay.textContent.toLowerCase().includes('lait'));
|
||||
});
|
||||
|
||||
test('la modale se ferme via le bouton de fermeture', () => {
|
||||
setupDom();
|
||||
openAllergenModal(loadAllergensFixture());
|
||||
document.querySelector('.allergen-modal-close').click();
|
||||
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
|
||||
});
|
||||
|
||||
test('la modale se ferme par clic sur l overlay (hors contenu)', () => {
|
||||
const dom = setupDom();
|
||||
const overlay = openAllergenModal(loadAllergensFixture());
|
||||
overlay.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true }));
|
||||
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
|
||||
});
|
||||
|
||||
test('la modale se ferme avec la touche Echap', () => {
|
||||
const dom = setupDom();
|
||||
openAllergenModal(loadAllergensFixture());
|
||||
document.dispatchEvent(new dom.window.KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
|
||||
});
|
||||
|
||||
test('ouvrir deux fois ne duplique pas la modale (idempotent)', () => {
|
||||
setupDom();
|
||||
const list = loadAllergensFixture();
|
||||
openAllergenModal(list);
|
||||
openAllergenModal(list);
|
||||
assert.equal(document.querySelectorAll('.allergen-modal-overlay').length, 1);
|
||||
closeAllergenModal();
|
||||
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
|
||||
});
|
||||
4
tests/js/package.json
Normal file
4
tests/js/package.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"//": "Les tests front borne sont en ESM (import). Scoping local pour ne pas imposer type:module a la racine.",
|
||||
"type": "module"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue