Export Web/SCORM : port complet du runtime jouable côté navigateur
Phase 1.2 de la feuille de route produit — un paquet SCORM tourne seul dans le LMS du client, sans serveur Forge Engine disponible. Ajoute static/js/play/offline/ (miroir JS de screens/rendering/ et data_actions/, sous window.FORGE_OFFLINE) pour que variables, score, données d'objet, répéteurs et conditions de visibilité fonctionnent entièrement en mémoire côté navigateur ; branche actions.js et bindings.js dessus au lieu d'un fetch() serveur. Ajoute publish/build_scorm_package.py (paquet statique + imsmanifest.xml SCORM 1.2 + wrapper API SCORM) et le bouton "Exporter (Web/SCORM)". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9776fbc057
commit
f3b72d73c9
@@ -0,0 +1,74 @@
|
||||
"""Export Web/SCORM (bouton "Exporter (Web/SCORM)", voir
|
||||
routes/publish/export_scorm.py et publish/build_scorm_package.py) —
|
||||
paquet STATIQUE (aucun Python/Flask embarqué, contrairement à
|
||||
publish/build_package.py testé dans test_publish.py) : ces tests
|
||||
vérifient l'assemblage (imsmanifest.xml, index.html en FORGE_OFFLINE,
|
||||
JS hors ligne présent), jamais un vrai import dans un LMS réel (voir le
|
||||
plan — validation ADL SCORM Cloud/Moodle hors du champ de la suite
|
||||
automatisée)."""
|
||||
import zipfile
|
||||
|
||||
from tests.test_auth import anon_client # noqa: F401
|
||||
|
||||
|
||||
def test_export_scorm_route_returns_a_zip_attachment(client, game):
|
||||
resp = client.post(f"/game/{game}/export-scorm")
|
||||
assert resp.status_code == 200
|
||||
assert resp.headers["Content-Type"] == "application/zip"
|
||||
assert "attachment" in resp.headers["Content-Disposition"]
|
||||
assert resp.data[:2] == b"PK"
|
||||
|
||||
|
||||
def test_export_scorm_zip_contains_the_expected_files(client, game):
|
||||
resp = client.post(f"/game/{game}/export-scorm")
|
||||
import io
|
||||
with zipfile.ZipFile(io.BytesIO(resp.data)) as zf:
|
||||
names = set(zf.namelist())
|
||||
for expected in [
|
||||
"imsmanifest.xml", "index.html",
|
||||
"static/style.css", "static/csrf_fetch.js",
|
||||
"static/js/play/actions.js", "static/js/play/bindings.js",
|
||||
"static/js/play/offline/full-payload.js",
|
||||
"static/js/play/offline/apply-actions.js",
|
||||
"static/js/play/offline/scorm-api.js",
|
||||
]:
|
||||
assert expected in names, f"{expected} manquant du paquet SCORM"
|
||||
# __tests__/ (node:test) n'a rien à faire dans un paquet livré au LMS.
|
||||
assert not any("__tests__" in n for n in names)
|
||||
|
||||
|
||||
def test_export_scorm_index_html_is_offline_and_self_contained(client, game):
|
||||
resp = client.post(f"/game/{game}/export-scorm")
|
||||
import io
|
||||
with zipfile.ZipFile(io.BytesIO(resp.data)) as zf:
|
||||
html = zf.read("index.html").decode("utf-8")
|
||||
assert "window.FORGE_OFFLINE = true" in html
|
||||
# Chemins RELATIFS (static/...) — un /static/... absolu casserait un
|
||||
# index.html ouvert en local (file://), sans racine de serveur.
|
||||
assert 'src="static/js/play/actions.js"' in html
|
||||
assert 'src="/static/' not in html
|
||||
assert "let gameData = " in html
|
||||
|
||||
|
||||
def test_export_scorm_manifest_lists_index_html_as_the_sco(client, game):
|
||||
resp = client.post(f"/game/{game}/export-scorm")
|
||||
import io
|
||||
with zipfile.ZipFile(io.BytesIO(resp.data)) as zf:
|
||||
manifest = zf.read("imsmanifest.xml").decode("utf-8")
|
||||
assert 'href="index.html"' in manifest
|
||||
assert "adlcp:scormtype=\"sco\"" in manifest
|
||||
|
||||
|
||||
def test_export_scorm_route_is_isolated_like_other_game_routes(anon_client):
|
||||
"""Même garde d'accès que /game/<slug>/publish (core/auth_guard.py) —
|
||||
aucune vérification supplémentaire écrite pour cette route."""
|
||||
from tests.test_auth import _register, _confirm_2fa, _complete_onboarding, _cleanup_project
|
||||
|
||||
_register(anon_client, "scormexportisolation@example.com")
|
||||
_confirm_2fa(anon_client)
|
||||
_complete_onboarding(anon_client)
|
||||
try:
|
||||
resp = anon_client.post("/game/un-projet-qui-nest-pas-le-sien/export-scorm")
|
||||
assert resp.status_code == 403
|
||||
finally:
|
||||
_cleanup_project("scormexportisolation@example.com")
|
||||
Reference in New Issue
Block a user