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:
@@ -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 % 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 $/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 924 $' in body or '3 924 $' in body
|
||||
# Wave solution card pricing
|
||||
assert '173 $/mois' in body or 'Dès 173' in body
|
||||
# Cadre — Loi 25 fine
|
||||
assert '25 M$' in body or '25 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
|
||||
|
||||
Reference in New Issue
Block a user