"""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(): """New 'Comment ça marche' interactive reactor section (post-6-features, pre-integrations). Validates structure (heading + reactor + spec list + Mistral card), 6 cycling features, canonical content, and a11y signals (aria-labelledby + aria-live status panel). """ 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) for feat in ['Transcription', 'Diarisation', '99+ langues', 'Exports', 'Utilisateurs illimités', 'Partage & Classement']: assert feat in body, f"Missing cycling feature: {feat}" # Reactor visual: rings, orbits, wordmark, Auto badge assert 'reactor-ring' in body and 'ring-outer' in body and 'ring-mid' in body and 'ring-inner' in body assert 'orbit orbit-1' in body and 'orbit orbit-8' in body, "Missing 8 orbital particles" assert '>DictIA<' in body, "Missing reactor centre wordmark" # Mistral 7B IA intégrée card with 3 canonical bullets 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 # Alpine reactor data + auto-cycle + hover/focus stop logic assert "active: \"Transcription\"" in body assert 'isHovered' in body assert 'setActive(feat)' in body assert 'resumeCycle()' in body # Hover, focus + blur listeners on each list item assert '@mouseenter="setActive(feat)"' in body assert '@mouseleave="resumeCycle()"' in body assert '@focus="setActive(feat)"' in body assert '@blur="resumeCycle()"' in body # 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" 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('