feat(marketing): round 2 — intègre 3 sections de dictia.ca/solutions/dictai (cycle/wave/cadre)

- Cycle "Trois options. Une seule est conforme." (entre PAS Problème & Solution)
  3 colonnes comparatives (humaine 315$/h / cloud US illégal / DictIA 173$/mo)
  Phases reveal 1→4 via IntersectionObserver + setTimeout chain
  Anneaux pulsants source node + horloge rotation + particules fuites cloud
  Overlay légal NON CONFORME sur col 2

- Wave "Onde de transformation" (entre Solution & Pipeline)
  Slider mouse-X interactif : 30 barres SVG morphent rouge → cyan
  Particules tombantes -$/-h (CSS keyframes staggered)
  Étiquettes douleur PAINS / SOLUTIONS flottantes
  Mobile : toggle button, pas de mouse interaction

- Cadre réglementaire "Moniteur d'Interception" (entre Conformité & Témoignages)
  Mappe 6 textes officiels : Loi 25, Loi 96, Cloud Act US, Guide IA Barreau, Cadre IA MCN, CAI
  Liens vers sources autoritaires (legisquebec, congress.gov, barreau, tresor, cai)
  HUD console typing reveal + caret blink + folder QC→US transition
  aria-live="polite" sur verdict, role="list" sur REGS

Texte 100% canonique extrait de Website-Sanity dictai-cycle/wave/contraste.tsx.
Toutes animations CSS pure + Alpine.js + IntersectionObserver natif (zéro lib JS externe).
prefers-reduced-motion désactive tout.
+802 lignes landing.html, +119 lignes tests (6 nouveaux test_round2_*), npm run build:css exécuté.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-28 12:21:16 -04:00
parent e49652d85d
commit 69baa1be2f
3 changed files with 1274 additions and 0 deletions

View File

@@ -754,6 +754,125 @@ def test_cta_final_uses_safe_pre_launch_wording():
assert phrase not in body, f"Forbidden pre-launch CTA: {phrase}"
def test_round2_cycle_section_present():
"""Round 2 — Cycle section ('Trois options. Une seule est conforme.') must be on landing.
Sourced from dictai-cycle.tsx; covers the 3-column comparative narrative
(humaine / cloud US / DictIA) with canonical pricing 315 $ vs 173 $ and savings.
"""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'cycle-title' in body, "Cycle section H2 id must be present"
assert 'Trois options' in body
assert 'Une seule est conforme' in body
assert 'Retranscription humaine' in body
assert 'IA cloud américaine' in body
assert 'NON CONFORME' in body
assert '315' in body and '173' in body, "Canonical Cycle pricing must appear"
assert 'Loi 25 conforme' in body
assert '100&nbsp;% hébergé au Québec' in body or '100 % hébergé au Québec' in body
# Phase animation hooks
assert 'cycle-pulse' in body, "Pulse rings keyframe class missing"
assert 'cycle-card-dictia' in body
# Reduced-motion safety
assert 'prefers-reduced-motion' in body
def test_round2_wave_section_present():
"""Round 2 — Wave section (chaos→ordre interactive slider) must be on landing.
Sourced from dictai-wave.tsx; mouse-X morphs 30 bars red→cyan + pain/solution cards.
"""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'wave-title' in body, "Wave section H2 id must be present"
assert 'La transcription manuelle' in body
assert 'coûte cher' in body
# Canonical pain labels
assert '4 à 6h pour transcrire 1h' in body
assert 'Délais de 48h à 5 jours' in body
# Canonical solution labels (NBSP-aware)
assert '~2 min pour 1h d' in body
assert '173&nbsp;$/mois' in body or '173 $/mois' in body
# Alpine state for interactive slider
assert 'onMove($event)' in body
assert 'isMobile' in body
# Mobile fallback toggle
assert 'Activer DictIA' in body
assert 'Voir sans DictIA' in body
def test_round2_cadre_reglementaire_section_present():
"""Round 2 — Cadre réglementaire (Moniteur d'Interception) with 6 REGS list.
Sourced from dictai-contraste.tsx (REGS + MoniteurInterception subcomponent).
"""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'cadre-title' in body, "Cadre réglementaire H2 id must be present"
assert "Moniteur d'Interception" in body
assert 'enfreignent' in body
# 6 REGS — each must appear with its hyperlink
for reg_label in ['Loi 25 (P-39.1)', 'Loi 96 (C-11)', 'US Cloud Act',
'Guide IA — Barreau QC', 'Cadre IA — MCN', 'CAI']:
assert reg_label in body, f"Missing REG label: {reg_label}"
# Authoritative sources
assert 'legisquebec.gouv.qc.ca' in body
assert 'cai.gouv.qc.ca' in body
assert 'tresor.gouv.qc.ca' in body
# HUD lines
assert 'Interception IA détectée' in body
assert 'NON CONFORME' in body
# Cycle animation hooks
assert 'cadre-folder' in body
assert 'runCycle' in body
def test_round2_no_external_js_libs_added():
"""Round 2 must NOT add Framer Motion / GSAP / canvas-confetti / etc."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
forbidden_libs = ['framer-motion', 'gsap', 'canvas-confetti', 'three.min.js',
'lottie-web', 'anime.min.js']
for lib in forbidden_libs:
assert lib not in body, f"Round 2 must not introduce JS lib: {lib}"
def test_round2_preserves_existing_sections():
"""Round 2 inserts must NOT remove Hero / Pipeline / Hub / Bento / Comparatif / Conformité."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Hero (round 0)
assert 'hero-title' in body
assert 'sans risquer votre permis' in body
# Pipeline (round 1) — auto-advance + 4 nodes
assert 'pipeline-title' in body
assert 'Du fichier au résumé' in body
# Hub (round 1)
assert 'hub-title' in body
assert 'se connecte à tout' in body
# Bento + ROI calculator
assert 'bento-title' in body
assert 'roiCalculator()' in body
# Comparatif + Conformité
assert 'comparatif-title' in body
assert 'conformite-title' in body
# Trust bar 9 ordres
assert 'Mappé aux 9' in body
def test_round2_oqlf_nbsp_in_new_sections():
"""OQLF — non-breaking space before currency $ and % in round 2 sections."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Cycle section savings
assert '3&nbsp;924&nbsp;$' in body or '3 924 $' in body
# Wave solution card pricing
assert '173&nbsp;$/mois' in body or 'Dès 173' in body
# Cadre — Loi 25 fine
assert '25 M$' in body or '25&nbsp;M$' in body
def test_routes_passes_testimonials_and_faq_to_template():
"""marketing.routes.landing() must pass testimonials and faq to render_template."""
# Import the module to verify the data lists exist