Au lieu d'un cycle générique 1→12, l'animation suit maintenant un FLOW PROCESSUS canonique qui raconte l'histoire produit du début à la fin : ÉTAPE 1 · CAPTURE → Recording (7) + Transcription (1) ÉTAPE 2 · TRANSFORMATION IA → Diarisation (2) + Langues (3) + Résumé (9) + Chat IA (0) ÉTAPE 3 · DISTRIBUTION → Exports (4) + Partage (6) + Users (5) + Intégrations (10) ÉTAPE 4 · GOUVERNANCE → Recherche IA (8) + Audit (11) + Conformité (12) PROCESS_ORDER = [7, 1, 2, 3, 9, 0, 4, 6, 5, 10, 8, 11, 12] — 13 sub-modes, mode 0 IA inclus dans le cycle (au lieu d'être skippé), 1100ms × 13 ≈ 14.3s tour complet. Composants ajoutés / refondus : - PROCESS BREADCRUMB visible au-dessus du phone (4 pilules · flèches · past/active/future) - PROGRESS BAR sous breadcrumb (gradient brand-b1 → b2 → b3, role=progressbar) - BOTTOM TAB BAR : 4 boutons étapes (au lieu de 6 features individuelles) - RIGHT PANEL : 4 mini-sections par étape (12 boutons regroupés selon flow) - FEATURE INFO CARD : préfixe 'Étape X/4 · TITRE' pour contexte processus - CONNECTING LINE : flèche directionnelle + dot animé qui se déplace selon processIdx - Mobile pills : suivent PROCESS_ORDER (13 sub-modes au lieu de 1-12) Alpine refactor : - selectedFeature devient un getter computed sur PROCESS_ORDER[processIdx] - nouveau STEPS array avec subModes mapping + activeStep / progressPercent getters - goToStep(id) helper jump au 1er sub-mode d'une étape - handleManualSelect(i) résout l'index dans PROCESS_ORDER Préservé : palette brand stricte (b1/b2/b3/navy), V3 radii, Inter+JetBrains Mono, phone shell statique, 13 templates de modes, IA Mistral premium card, eyebrow 'COMMENT ÇA MARCHE' brand-navy, WCAG (aria-current, aria-label, role=navigation, role=progressbar, role=tablist), prefers-reduced-motion guards. Tests : 9/9 fonctionnalites passent (test how_it_works_reactor étendu de +37 assertions narratives : PROCESS_ORDER, STEPS, processIdx, activeStep, goToStep, breadcrumb, progressbar, step-bottom, step-section, contexte processus dans card). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
544 lines
25 KiB
Python
544 lines
25 KiB
Python
"""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 '<!DOCTYPE html>' 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 '<h1' in body and 'choisissez votre infrastructure' in body
|
||
|
||
|
||
def test_tarifs_renders_4_pricing_cards_v7():
|
||
"""Tarifs page renders the v7.0 3 Cloud forfaits + DictIA LOCAL dedicated block + Pro+ chip."""
|
||
client = app.test_client()
|
||
body = client.get('/tarifs').data.decode('utf-8')
|
||
for tier in ['Cloud BASIC', 'Cloud ESSENTIEL', 'Cloud PRO', 'DictIA LOCAL']:
|
||
assert tier in body
|
||
# Canonical NBSP prices (v7.0)
|
||
assert '189 $' in body
|
||
assert '349 $' in body
|
||
assert '549 $' in body
|
||
assert '485 $' in body # Cloud Pro onboarding
|
||
assert '5 998 $' in body # DictIA Local An 1
|
||
# 3 Cloud forfaits use checkout slugs
|
||
assert 'href="/checkout/cloud-basic"' in body
|
||
assert 'href="/checkout/cloud-essentiel"' in body
|
||
assert 'href="/checkout/cloud-pro"' in body
|
||
# DictIA LOCAL has its own dedicated block with a contact CTA (no /checkout slug)
|
||
assert '/contact?plan=dictia-local' in body, "Missing DictIA LOCAL block contact CTA"
|
||
assert 'Vous en êtes' in body or 'Vous en êtes' in body, \
|
||
"Missing DictIA LOCAL block headline 'Vous en êtes propriétaire'"
|
||
assert 'Serveur DictIA' in body, "Missing DictIA LOCAL block server visual mockup label"
|
||
# Pro+ chip with /contact link
|
||
assert 'Pro+' in body
|
||
assert '/contact?pro-plus=1' in body
|
||
|
||
|
||
def test_tarifs_comparison_matrix_v7():
|
||
"""v7.0 comparison matrix has 4 columns + 10 rows."""
|
||
client = app.test_client()
|
||
body = client.get('/tarifs').data.decode('utf-8')
|
||
assert 'matrix-title' in body
|
||
assert '<caption class="sr-only">' 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 '<!DOCTYPE html>' 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 '1100' in body, "Missing 1100ms sub-mode cycle interval (refonte 2026-04-29)"
|
||
assert '4500' in body, "Missing 4500ms manual reset timer"
|
||
# Refonte 2026-04-29 (v2) : 12 fonctions visibles directement (1-12), IA mode 0 via Brain card.
|
||
# Bottom tab bar = 6 boutons originaux (Trans/Diari/Lang/Exp/Users/Part) ; right panel grid = 12 features.
|
||
assert 'featureGridLabel' in body, "Missing featureGridLabel (right grid 12 buttons)"
|
||
assert '12 fonctions' in body, "Missing '12 fonctions' badge in right panel header"
|
||
# Labels compacts grid right panel
|
||
for grid_label in ['Recording', 'Recherche IA', 'Résumés', 'Intégrations', 'Audit trail', 'Conformité']:
|
||
assert grid_label in body, f"Missing right grid label : {grid_label}"
|
||
# Nouveaux modes 7-12
|
||
assert 'recModeData' in body, "Missing Mode 7 (Recording live)"
|
||
assert 'searchModeData' in body, "Missing Mode 8 (Recherche sémantique)"
|
||
assert 'summaryModeData' in body, "Missing Mode 9 (Résumé + actions)"
|
||
assert 'integModeData' in body, "Missing Mode 10 (Intégrations Hub)"
|
||
assert 'auditModeData' in body, "Missing Mode 11 (Audit trail)"
|
||
assert 'loi25ModeData' in body, "Missing Mode 12 (Conformité Loi 25)"
|
||
# Mode panels content keywords
|
||
assert 'ENREGISTREMENT LIVE' in body, "Missing recording header"
|
||
assert 'RECHERCHE IA SÉMANTIQUE' in body, "Missing semantic search header"
|
||
assert 'RÉSUMÉ EXÉCUTIF' in body, "Missing summary header"
|
||
assert 'INTÉGRATIONS HUB' in body, "Missing integrations hub header"
|
||
assert 'AUDIT TRAIL' in body, "Missing audit trail header"
|
||
assert 'CONFORMITÉ QUÉBEC' in body, "Missing conformity header"
|
||
assert 'ÉVÉNEMENT DÉTECTÉ' in body, "Missing ICS event detection"
|
||
assert '9 ordres professionnels' in body, "Missing 9 ordres listing"
|
||
for ordre in ['Barreau', 'CNQ', 'CPA', 'OIIQ']:
|
||
assert ordre in body, f"Missing ordre pro: {ordre}"
|
||
# 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 (refonte v2 2026-04-29 : 12 fonctions visibles)
|
||
assert 'modules' in body, "Missing stats row 'modules'"
|
||
assert 'fonctions' in body, "Missing stats row 'fonctions' (refonte v2)"
|
||
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("</script>", 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"
|
||
|
||
# ── REFONTE NARRATIVE PROCESSUS 2026-04-29 v3 ──────────────────────────
|
||
# PROCESS_ORDER : ordre canonique du flow utilisateur (13 sub-modes, mode 0 inclus)
|
||
assert 'PROCESS_ORDER' in body, "Missing PROCESS_ORDER array (narrative refactor)"
|
||
assert '[7, 1, 2, 3, 9, 0, 4, 6, 5, 10, 8, 11, 12]' in body, \
|
||
"Missing canonical PROCESS_ORDER sequence (Capture → Transform IA → Distribution → Gouvernance)"
|
||
# 4 STEPS (étapes principales) + helpers
|
||
assert 'STEPS:' in body, "Missing STEPS array"
|
||
assert "'Capture'" in body, "Missing étape 1 Capture"
|
||
assert "'Transformation IA'" in body, "Missing étape 2 Transformation IA"
|
||
assert "'Distribution'" in body, "Missing étape 3 Distribution"
|
||
assert "'Recherche & Gouvernance'" in body, "Missing étape 4 Recherche & Gouvernance"
|
||
assert 'subModes:' in body, "Missing subModes mapping"
|
||
# Computed states + helpers
|
||
assert 'processIdx' in body, "Missing processIdx state"
|
||
assert 'activeStep' in body, "Missing activeStep getter"
|
||
assert 'goToStep' in body, "Missing goToStep helper"
|
||
assert 'progressPercent' in body, "Missing progressPercent getter"
|
||
# Process breadcrumb visible above phone
|
||
assert 'dictia-process-breadcrumb' in body, "Missing process breadcrumb wrapper"
|
||
assert 'Étapes du processus DictIA' in body, "Missing breadcrumb aria-label"
|
||
assert 'aria-current' in body, "Missing aria-current on active step"
|
||
# Progress bar
|
||
assert 'role="progressbar"' in body, "Missing progressbar role"
|
||
assert 'Progression du flow' in body, "Missing progress bar label"
|
||
# Bottom step navigator (4 buttons étapes au lieu de 6 features)
|
||
assert 'dictia-step-bottom' in body, "Missing bottom step navigator class"
|
||
assert 'Étapes du processus' in body, "Missing bottom tablist aria-label"
|
||
# Right panel : 4 mini-sections par étape
|
||
assert 'dictia-step-section' in body, "Missing right panel step sections"
|
||
# Feature info card : context processus (Étape X/4 · Titre)
|
||
assert 'STEPS[activeStep - 1]' in body, "Missing process context binding in feature card"
|
||
# Connecting line : travelling dot following processIdx
|
||
assert 'PROCESS_ORDER.length' in body, "Missing PROCESS_ORDER.length binding (travelling dot or progress)"
|
||
# Right panel header : updated badge
|
||
assert '12 fonctions · 4 étapes' in body, "Missing right panel updated badge"
|
||
|
||
|
||
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 '<!DOCTYPE html>' in body
|
||
assert 'lang="fr-CA"' in body
|
||
assert 'page-title' in body
|
||
assert body.count('<h1') == 1, "Must have exactly one H1"
|
||
|
||
|
||
def test_conformite_4_pillars_present():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert 'pillars-title' in body
|
||
# 4 pillars (same anchors as landing's conformité section)
|
||
pillar_keywords = ['OVH Beauharnois', 'LPRPSP', 'LGGRI', 'AGPL']
|
||
for kw in pillar_keywords:
|
||
assert kw in body, f"Conformité pillar missing: {kw}"
|
||
# Soft hedges (LPC art. 219)
|
||
assert 'Mapp' in body, "Must use 'Mappé' (not 'Certifié')"
|
||
assert 'selon le périmètre' in body or 'selon le périmètre' in body, \
|
||
"SOC 2 must be hedged"
|
||
|
||
|
||
def test_conformite_loi25_3_articles():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert 'loi25-title' in body
|
||
# 3 LPRPSP articles
|
||
for art in ['Art. 3.3', 'Art. 3.5', 'Art. 14']:
|
||
assert art in body, f"Conformité missing article: {art}"
|
||
# Article topics
|
||
for topic in ['EFVP', 'Audit trail', 'Consentement']:
|
||
assert topic in body, f"Conformité missing Loi 25 topic: {topic}"
|
||
# I-1 fix: badge must use solid bg-brand-navy (not grad-bg) — WCAG AA
|
||
# text-on-grad-bg only achieves ~2:1 contrast on the cyan/green portion
|
||
assert 'bg-brand-navy text-white text-xs font-black' in body, \
|
||
"Loi 25 badges must use solid navy background for WCAG AA contrast (not grad-bg)"
|
||
# The grad-bg should NOT appear with the small badge text combo
|
||
assert 'grad-bg text-white text-xs font-black' not in body, \
|
||
"Removed grad-bg + small white text combo (WCAG AA fail)"
|
||
|
||
|
||
def test_conformite_agpl_section_with_external_links():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert 'agpl-title' in body
|
||
assert 'AGPL' in body
|
||
# External Gitea link with rel=noopener
|
||
assert 'gitea.innova-ai.ca/Innova-AI/dictia-public' in body
|
||
assert 'rel="noopener"' in body
|
||
# Link to GNU AGPL v3 official text
|
||
assert 'gnu.org/licenses/agpl-3.0' in body
|
||
|
||
|
||
def test_conformite_uses_oqlf_typography():
|
||
client = app.test_client()
|
||
body = client.get('/conformite').data.decode('utf-8')
|
||
assert 'Loi 25' in body
|
||
assert 'AGPL v3' in body
|
||
assert 'art. 3.5' in body or 'Art. 3.5' in body
|
||
assert '&nbsp;' not in body, "Macro | safe regression"
|
||
|
||
|
||
# === /contact ===
|
||
|
||
def test_contact_route_returns_200():
|
||
client = app.test_client()
|
||
response = client.get('/contact')
|
||
assert response.status_code == 200
|
||
|
||
|
||
def test_contact_route_does_not_accept_post_yet():
|
||
"""POST handler is B-2.x territory — for now, /contact is GET-only."""
|
||
client = app.test_client()
|
||
response = client.post('/contact', data={'name': 'test'})
|
||
assert response.status_code == 405, "POST must return 405 until B-2.x form handler ships"
|
||
|
||
|
||
def test_contact_extends_base_with_h1():
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert '<!DOCTYPE html>' in body
|
||
assert 'page-title' in body
|
||
assert body.count('<h1') == 1
|
||
|
||
|
||
def test_contact_3_methods_email_phone_address():
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert 'methods-title' in body
|
||
# Email
|
||
assert 'href="mailto:info@dictia.ca"' in body
|
||
# Phone (tel: link)
|
||
assert 'href="tel:+15819968471"' in body
|
||
assert '(581) 996-8471' in body
|
||
# Address (postal code)
|
||
assert 'G0S 1K0' in body
|
||
assert 'Inverness' in body
|
||
assert '<address' in body
|
||
|
||
|
||
def test_contact_6_subject_shortcuts():
|
||
"""Pre-filled mailto subject shortcuts for common requests."""
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert 'shortcuts-title' in body
|
||
# 6 shortcut anchors with mailto + subject
|
||
expected_subjects = [
|
||
'Pr%C3%A9-inscription%20DictIA',
|
||
'Devis%20multi-sites',
|
||
'Demande%20de%20d%C3%A9monstration',
|
||
'Dossier%20conformit%C3%A9%20Loi%2025',
|
||
'Partenariat',
|
||
'Question%20m%C3%A9dia',
|
||
]
|
||
for subj in expected_subjects:
|
||
assert subj in body, f"Missing mailto subject shortcut: {subj}"
|
||
# Each shortcut is a focusable link with focus-visible
|
||
assert 'focus-visible:outline-2' in body
|
||
assert 'focus-visible:outline-brand-b1' in body
|
||
|
||
|
||
def test_contact_pre_launch_form_disclaimer():
|
||
"""Until B-2.x ships the form handler, the page must clearly disclose mailto-only."""
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert 'Formulaire en ligne' in body
|
||
assert 'lancement' in body or 'printemps' in body, \
|
||
"Must indicate online form is coming at launch"
|
||
|
||
|
||
def test_contact_uses_oqlf_typography():
|
||
client = app.test_client()
|
||
body = client.get('/contact').data.decode('utf-8')
|
||
assert '2 jours' in body
|
||
assert '9 h' in body
|
||
assert '&nbsp;' not in body
|