"""Tests for the secondary marketing pages (A-2.8).""" import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:') os.environ.setdefault('SECRET_KEY', 'test-secret-key') from src.app import app # noqa: E402 # === /tarifs === def test_tarifs_route_returns_200(): client = app.test_client() response = client.get('/tarifs') assert response.status_code == 200, "GET /tarifs must return 200" def test_tarifs_extends_marketing_base(): client = app.test_client() body = client.get('/tarifs').data.decode('utf-8') assert '' in body assert 'lang="fr-CA"' in body assert '/static/css/marketing.css' in body assert '/static/js/alpine.min.js' in body def test_tarifs_has_h1_with_anchor(): client = app.test_client() body = client.get('/tarifs').data.decode('utf-8') assert 'page-title' in body assert '' in body assert 'scope="col"' in body assert 'scope="row"' in body # v7.0 row keywords (matches the rows in tarifs.html) for kw in ['Hébergement', 'GPU', 'Capacité audio', 'Stockage', 'Utilisateurs', 'Diarisation pyannote', 'Loi 25', 'SLA', 'Délai']: assert kw in body, f"Missing matrix row keyword: {kw}" def test_tarifs_pricing_faq_v7(): """v7.0 tarifs FAQ has 7 questions (added DictIA Local + Cloud Pro onboarding + Pro+ explanations).""" client = app.test_client() body = client.get('/tarifs').data.decode('utf-8') assert 'tarifs-faq-title' in body for i in range(1, 8): assert f'tarifs-faq-panel-{i}' in body, f"Missing tarifs FAQ panel {i}" # Alpine accordion bindings assert body.count('x-data="{ open: false }"') >= 7 # Each accordion button has focus-visible (WCAG 2.4.7/2.4.11) assert 'focus-visible:outline-2' in body def test_tarifs_uses_oqlf_typography(): client = app.test_client() body = client.get('/tarifs').data.decode('utf-8') assert 'TPS 5 %' in body assert 'TVQ 9,975 %' in body assert 'Loi 25' in body # No double-escape assert '&nbsp;' not in body, "Pricing macro / safe filter regression" # === /fonctionnalites === def test_fonctionnalites_route_returns_200(): client = app.test_client() response = client.get('/fonctionnalites') assert response.status_code == 200 def test_fonctionnalites_extends_marketing_base(): client = app.test_client() body = client.get('/fonctionnalites').data.decode('utf-8') assert '' in body assert 'lang="fr-CA"' in body assert '/static/css/marketing.css' in body def test_fonctionnalites_h1_present(): client = app.test_client() body = client.get('/fonctionnalites').data.decode('utf-8') assert 'page-title' in body assert 'rester' in body or 'restant chez soi' in body def test_fonctionnalites_renders_6_bento_cards(): client = app.test_client() body = client.get('/fonctionnalites').data.decode('utf-8') assert 'features-title' in body # 6 watermark numbers for n in ['01', '02', '03', '04', '05', '06']: assert f'>{n}<' in body # 6 feature anchors for kw in ['WhisperX', 'Diarisation', 'Mistral 7B', 'RAG local', 'DOCX, PDF, SRT', 'Outlook, Teams']: assert kw in body def test_fonctionnalites_how_it_works_reactor_section(): """'Comment ça marche' interactive section — fidèle reproduction du composant DashboardHolographique (Website-Sanity/dictai-narrative.tsx). Validates structure (phone container + 6 modes + IA Mistral card + 6 feature grid), auto-cycle + manual click logic, canonical content, and a11y signals. """ client = app.test_client() body = client.get('/fonctionnalites').data.decode('utf-8') # Section heading + canonical phrasing assert 'how-it-works-title' in body, "Missing how-it-works section anchor" assert 'COMMENT ÇA MARCHE' in body, "Missing canonical eyebrow" assert 'Du fichier au résumé' in body, "Missing canonical H2 phrasing" assert 'en temps réel' in body assert 'Survolez une fonctionnalité pour voir la machine en action' in body # 6 cycling features (canonical names — appears in FEATURES JS array) for feat in ['Transcription', 'Diarisation', '99+ langues', 'Exports', 'Utilisateurs illimités']: assert feat in body, f"Missing cycling feature: {feat}" # Partage & Classement (avec & littéral dans le JS) assert "'Partage & Classement'" in body, "Missing Partage & Classement feature" # Phone container : Alpine root + features + Mic pulsing + DictIA logo assert 'dictiaDashboard()' in body, "Missing dictiaDashboard Alpine root" assert 'dictia-mic-pulse' in body, "Missing pulsing Mic animation class" assert 'dictia-logo-nom.png' in body, "Missing DictIA logo image" # 6 modes uniques (chacun a son sub-data function ou sa signature unique) assert 'trModeData()' in body, "Missing Mode 1 (Transcription) data function" assert 'reunion-jan14.mp3' in body, "Missing transcription upload filename" assert 'diaModeData()' in body, "Missing Mode 2 (Diarisation) data function" assert 'Sophie' in body and 'Marc' in body and 'Julie' in body, "Missing diarisation speakers" assert 'langModeData()' in body, "Missing Mode 3 (Langues) data function" assert '99+ langues détectées' in body, "Missing langues subtitle" assert 'expModeData()' in body, "Missing Mode 4 (Exports) data function" assert 'usersModeData()' in body, "Missing Mode 5 (Users) data function" assert 'iaModeData()' in body, "Missing Mode 0 (IA chat) data function" assert 'MISTRAL 7B · LOCAL' in body, "Missing IA chat header label" # Mode 6 (Share) : folders + tags + files assert 'CR-Réunion-Jan14' in body, "Missing share file name" assert '#Confidentiel' in body, "Missing share tag" assert 'partage sécurisé' in body, "Missing share footer text" # IA Mistral 7B premium card avec 3 bullets souveraineté assert 'Mistral 7B' in body assert 'IA intégrée' in body assert 'Données hébergées sur VOS serveurs' in body assert 'Zéro connexion OpenAI' in body assert 'Inférence hors-ligne' in body assert 'ia-ambient-glow' in body, "Missing IA ambient glow animation" assert 'ia-brain-glow' in body, "Missing IA brain glow animation" assert 'ia-local-badge' in body, "Missing LOCAL badge pulsing animation" # Auto-cycle + manual select logic assert 'startAutoCycle' in body, "Missing auto-cycle function" assert 'handleManualSelect' in body, "Missing manual select handler" assert '900' in body, "Missing 900ms cycle interval" assert '4500' in body, "Missing 4500ms manual reset timer" # Auto / Manual status text assert 'Auto' in body, "Missing Auto status indicator" assert 'Auto reprend bientôt' in body, "Missing manual mode hint" # Accessibility: aria-live status panel, prefers-reduced-motion guard assert 'aria-live="polite"' in body assert 'prefers-reduced-motion' in body, "Reduced-motion guard missing" assert 'aria-pressed' in body, "Missing aria-pressed on feature buttons" # ── HYPER PRO polish refactor 2026-04-29 ── ---------------------------- # Phone shell : bezel + notch + speaker + status bar assert 'dictia-phone-shell' in body, "Missing phone shell class" assert 'dictia-notch' in body, "Missing notch element" assert 'dictia-statusbar' in body, "Missing mobile-style status bar" assert '9:41' in body, "Missing 9:41 status bar time" assert 'dictia-sound-ring' in body, "Missing sound ring animation" assert 'dictia-phone-glow-ring' in body, "Missing phone external glow ring" # Mode 1 — REC indicator + waveform assert 'REC' in body, "Missing transcription REC indicator" assert 'dictia-rec-dot' in body, "Missing REC pulsing dot" assert 'dictia-wave-bar' in body, "Missing waveform animation" # Mode 2 — typing indicator + timestamps + stacked avatars assert 'dictia-typing-dot' in body, "Missing typing indicator dots" assert 'Réunion · 3 participants' in body, "Missing diarisation header" # Mode 3 — DÉTECTION AUTOMATIQUE header + ripple from center + counter assert 'DÉTECTION AUTOMATIQUE' in body, "Missing langues detection header" assert 'rippleDelay' in body, "Missing ripple delay function" assert 'HIGHLIGHTS' in body, "Missing langues highlights array" # Mode 4 — header EXPORTS + checkmark assert 'EXPORTS DISPONIBLES' in body, "Missing exports header" assert '7 FORMATS PRÊTS' in body, "Missing exports checkmark subtitle" # Mode 5 — counter centered + USER_COLORS assert 'UTILISATEURS' in body, "Missing users counter header" assert 'USER_COLORS' in body, "Missing users color variations" # Mode 6 — breadcrumb + toolbar assert 'Mes dossiers' in body, "Missing share breadcrumb" # BOTTOM zone — feature buttons + AUTO pill + countdown assert 'featureShortLabel' in body, "Missing feature short labels function" assert 'dictia-auto-pulse' in body, "Missing auto pill pulse" assert 'dictia-countdown-fill' in body, "Missing manuel countdown bar" # Right panel — IA card metrics + Brain in 40x40 circle assert 'MISTRAL 7B' in body, "Missing Mistral badge label" assert 'latence' in body, "Missing latency metric label" # Section background + connecting line assert 'dictia-bg-grid' in body, "Missing background grid pattern" assert 'dictia-bg-orbs' in body, "Missing floating orbs" assert 'dictia-connecting-line' in body, "Missing connecting line phone↔IA" # Stats row in section header assert 'modules' in body, "Missing stats row 'modules'" assert 'cloud' in body, "Missing stats row 'cloud'" assert 'Voir une démo' in body, "Missing demo CTA link" # ── HYPER PRO polish 2026-04-29 (audit purge non-brand hex + uniformization) ── # Brand canonical palette ONLY in JS color tables (no purple-600/cyan-700/blue-700/...) js_section = body[body.find("function dictiaDashboard()"):body.find("", body.find("function dictiaDashboard()"))] forbidden_hex = ['#9333ea', '#1d4ed8', '#a21caf', '#0e7490', '#0891b2', '#1e40af', '#67e8f9', '#93c5fd', '#f5d0fe', '#e879f9', '#A78BFA', '#22D3EE', '#6B9FFF', '#34D399', '#F59E0B', '#7C3AED', '#5B21B6', '#065F46', '#1C3A5E'] for hx in forbidden_hex: assert hx not in body, f"Forbidden non-brand hex {hx} found in fonctionnalites — must purge" # Inner screen seam (effet "écran encastré") assert 'dictia-phone-screen-seam' in body, "Missing inner screen seam (encastré effect)" # Uniform feature info card — bg-brand-navy SOLIDE (extension visuelle du phone, WCAG AA garanti sur section claire) + accent border-left assert 'dictia-feature-card rounded-xl px-4 py-3 relative bg-brand-navy' in body, \ "Missing solid bg-brand-navy on feature info card (must be dark, not transparent on light bg)" # Sovereignty bullets : icon dans cercle 20×20 brand-b3/[0.15] assert 'bg-brand-b3/[0.15]' in body, "Missing brand-b3 circle bg on sovereignty bullets" # Performance metrics : 3 cells with grad-text for 0ms / 24/7 assert 'grad-text' in body, "Missing grad-text on metrics" def test_fonctionnalites_export_formats_section(): client = app.test_client() body = client.get('/fonctionnalites').data.decode('utf-8') assert 'exports-title' in body for ext in ['DOCX', 'PDF', 'SRT', 'VTT', 'TXT', 'JSON', 'MD']: # Each format has its own card with the .ext as a heading assert f'>{ext}<' in body, f"Missing export format card: {ext}" def test_fonctionnalites_integrations_grid(): client = app.test_client() body = client.get('/fonctionnalites').data.decode('utf-8') assert 'integrations-title' in body for name in ['Microsoft Word', 'Microsoft Outlook', 'Microsoft Teams', 'Notion', 'Obsidian', 'Zapier', 'Make', 'n8n']: assert name in body, f"Missing integration: {name}" # Trademark disclaimer assert 'marques de leurs propriétaires' in body or 'marques de leurs propriétaires' in body def test_fonctionnalites_tech_specs_6_items(): client = app.test_client() body = client.get('/fonctionnalites').data.decode('utf-8') assert 'specs-title' in body for spec_keyword in ['Modèle ASR', 'pyannote', 'Mistral 7B', 'Flask', 'WAV, MP3', 'québécois']: assert spec_keyword in body, f"Missing tech spec keyword: {spec_keyword}" def test_fonctionnalites_uses_oqlf_typography(): client = app.test_client() body = client.get('/fonctionnalites').data.decode('utf-8') # NBSP entities assert '95 %+' in body, "WhisperX precision NBSP entity" # No double-escape assert '&nbsp;' not in body # === Cross-page checks === def test_secondary_pages_in_main_nav(): """Header nav links to /tarifs and /fonctionnalites — verify both pages now respond.""" client = app.test_client() for url in ['/tarifs', '/fonctionnalites']: # Each page should self-link in its own nav (consistency) body = client.get(url).data.decode('utf-8') assert 'href="/tarifs"' in body and 'href="/fonctionnalites"' in body, \ f"Page {url} must include nav links to both new pages" def test_secondary_pages_have_canonical_meta(): """Both pages must have canonical URL + OG metadata via base.html.""" client = app.test_client() for url in ['/tarifs', '/fonctionnalites']: body = client.get(url).data.decode('utf-8') assert 'rel="canonical"' in body assert 'og:type' in body assert 'twitter:card' in body # === /conformite === def test_conformite_route_returns_200(): client = app.test_client() response = client.get('/conformite') assert response.status_code == 200 def test_conformite_extends_base_with_h1(): client = app.test_client() body = client.get('/conformite').data.decode('utf-8') assert '' in body assert 'lang="fr-CA"' in body assert 'page-title' in body assert body.count('' in body assert 'page-title' in body assert body.count('